From c97042bf6a1e2f144fa3d212ab9b3a4efe6638b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:25:04 -0700 Subject: [PATCH 001/322] =?UTF-8?q?revert:=20"feat(rust):=20Observables,?= =?UTF-8?q?=20Events=20and=20IEnumerable=20(PR=206=20of=207=20Rust=20impro?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 20175eea841adf14ed57c0068458fad591c8d5f4. --- src/Fable.Transforms/Rust/Fable2Rust.fs | 198 +-------- src/Fable.Transforms/Rust/Replacements.fs | 106 +---- src/fable-library-rust/src/Async.rs | 11 +- src/fable-library-rust/src/Environment.rs | 21 - src/fable-library-rust/src/Event.rs | 293 -------------- .../src/Fable.Library.Rust.fsproj | 2 - src/fable-library-rust/src/File.rs | 28 -- src/fable-library-rust/src/Interfaces.fs | 24 -- src/fable-library-rust/src/Native.rs | 42 -- src/fable-library-rust/src/Observable.rs | 375 ------------------ src/fable-library-rust/src/Path.rs | 121 ------ src/fable-library-rust/src/Quotation.fs | 319 --------------- src/fable-library-rust/src/QuotationTypes.fs | 65 --- src/fable-library-rust/src/Reflection.rs | 228 +---------- src/fable-library-rust/src/lib.fs | 5 - tests/Rust/Fable.Tests.Rust.fsproj | 10 +- tests/Rust/tests/src/EnumerableTests.fs | 6 +- tests/Rust/tests/src/EnvironmentTests.fs | 22 - tests/Rust/tests/src/EventTests.fs | 165 ++++---- tests/Rust/tests/src/ObservableTests.fs | 29 +- tests/Rust/tests/src/QuotationTests.fs | 356 ----------------- tests/Rust/tests/src/RecordReflectionTests.fs | 125 ------ tests/Rust/tests/src/SystemIOTests.fs | 83 ---- 23 files changed, 111 insertions(+), 2523 deletions(-) delete mode 100644 src/fable-library-rust/src/Environment.rs delete mode 100644 src/fable-library-rust/src/Event.rs delete mode 100644 src/fable-library-rust/src/File.rs delete mode 100644 src/fable-library-rust/src/Observable.rs delete mode 100644 src/fable-library-rust/src/Path.rs delete mode 100644 src/fable-library-rust/src/Quotation.fs delete mode 100644 src/fable-library-rust/src/QuotationTypes.fs delete mode 100644 tests/Rust/tests/src/EnvironmentTests.fs delete mode 100644 tests/Rust/tests/src/QuotationTests.fs delete mode 100644 tests/Rust/tests/src/RecordReflectionTests.fs delete mode 100644 tests/Rust/tests/src/SystemIOTests.fs diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 4bfad14e52..76c2f6bb33 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -619,13 +619,6 @@ module TypeInfo = -> true | _ -> false - // A reference-typed (non-struct) record or union. Such entities are Lrc-wrapped - // at the value level, so boxing/unboxing them to `obj` needs a smart-pointer - // coercion rather than the plain value-type box (see transformCast). - let isReferenceRecordOrUnion (com: IRustCompiler) (entRef: Fable.EntityRef) = - let ent = com.GetEntity(entRef) - (ent.IsFSharpRecord || ent.IsFSharpUnion) && not ent.IsValueType - // Checks whether the type needs a ref counted wrapper // such as Rc (or Arc in a multithreaded context) let shouldBeRefCountWrapped (com: IRustCompiler) ctx typ = @@ -639,15 +632,12 @@ module TypeInfo = // always not Rc-wrapped | Fable.Unit | Fable.Measure _ + | Fable.MetaType | Fable.Boolean | Fable.Char | Fable.Number _ -> None // should be Rc-wrapped - // MetaType (System.Type) is erased to Any and carries a boxed reflection - // object at runtime, so it must be Lrc-wrapped like Any (a bare `dyn Any` - // is unsized and cannot appear as a value/argument). - | Fable.MetaType | Fable.Any | Fable.Regex | Replacements.Util.Builtin(Replacements.Util.FSharpReference _) @@ -1138,11 +1128,7 @@ module TypeInfo = | Fable.Char -> primitiveType "char" | Fable.Boolean -> primitiveType "bool" | Fable.String -> transformStringType com ctx - // System.Type has no faithful runtime on Rust (reflection is a stub), so erase it - // to Any. This lets the NewRecord quotation deconstruction bind its type slot to the - // boxed value the quotation runtime returns; typeof-based reflection is unsupported - // regardless (see the disabled ReflectionTests). - | Fable.MetaType -> transformAnyType com ctx + | Fable.MetaType -> transformMetaType com ctx | Fable.Number(kind, _) -> transformNumberType com ctx kind | Fable.LambdaType(argType, returnType) -> let argTypes, returnType = ([ argType ], returnType) @@ -1177,12 +1163,6 @@ module TypeInfo = // implemented random type | Replacements.Util.IsEntity (Types.random) (_, []) -> transformImportType com ctx [] "Random" "Random" - // implemented event type (FSharpEvent`1); the module functions and - // the .ctor are routed to Event_ by the events replacement, so the - // concrete type must resolve to Event_::FSharpEvent`1 as well. - | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`1") (_, [ t ]) -> - transformImportType com ctx [ t ] "Event" "FSharpEvent`1" - // implemented regex types | Replacements.Util.IsEntity (Types.regexMatch) (_, []) -> transformImportType com ctx [] "RegExp" "Match" | Replacements.Util.IsEntity (Types.regexGroup) (_, []) -> transformImportType com ctx [] "RegExp" "Group" @@ -1204,52 +1184,6 @@ module TypeInfo = // built-in types | Replacements.Util.Builtin kind -> transformBuiltinType com ctx typ kind - // Typed quotation Expr<'T>: erase the type argument so it shares the untyped - // FSharpExpr runtime representation. This lets a typed quotation (e.g. <@ 42 @>) - // be matched directly against the Patterns active patterns, which operate on - // the untyped Expr. - | Fable.DeclaredType(entRef, _) when entRef.FullName = Types.fsharpExprGeneric -> - transformEntityType com ctx { entRef with FullName = Types.fsharpExpr } [] - - // UnionCaseInfo (from a NewUnionCase quotation deconstruction) is erased to the - // Rust-native FSharpUnionCaseInfo carrier so uci.Name/uci.Tag resolve to the - // quotation runtime accessors. We build the import path directly (rather than via - // transformEntityType) because that carrier type does not exist in FSharp.Core, so - // com.GetEntity on the renamed ref would fail; getEntityFullName imports the type - // from fable_library_rust purely by FullName. - | Fable.DeclaredType(entRef, _) when entRef.FullName = "Microsoft.FSharp.Reflection.UnionCaseInfo" -> - let entName = - getEntityFullName - com - ctx - { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpUnionCaseInfo" } - - makeFullNamePathTy entName None - - // System.Reflection.MethodInfo (from a Call quotation deconstruction) is erased to - // the Rust-native FSharpMethodInfo carrier so mi.Name / mi.DeclaringType resolve to - // the quotation runtime accessors. Built directly by FullName (mirrors the - // UnionCaseInfo case) because that carrier type does not exist in FSharp.Core. - | Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.MethodInfo" -> - let entName = - getEntityFullName com ctx { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpMethodInfo" } - - makeFullNamePathTy entName None - - // System.Reflection.PropertyInfo is erased to the Rust-native FSharpPropertyInfo - // carrier, so p.Name / p.GetValue and FSharpValue.GetRecordField resolve to the - // reflection runtime. One carrier serves both reflection (GetRecordFields) and - // quotations (PropertyGet's propInfo), as in .NET. Built directly by FullName - // (mirrors the MethodInfo case) because that carrier does not exist in FSharp.Core. - | Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.PropertyInfo" -> - let entName = - getEntityFullName - com - ctx - { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpPropertyInfo" } - - makeFullNamePathTy entName None - // other declared types | Fable.DeclaredType(entRef, genArgs) -> transformEntityType com ctx entRef genArgs @@ -1625,17 +1559,6 @@ module Util = // unboxing value types or wrapped types | Fable.Any, t when isValueType com t || isWrappedType com t -> expr |> unboxValue com ctx t - // boxing a reference-typed record/union (Lrc-wrapped) to obj: coerce the - // smart pointer to `dyn Any` so its concrete pointee stays the struct. - | Fable.DeclaredType(entRef, _), Fable.Any when isReferenceRecordOrUnion com entRef -> - [ expr ] |> makeLibCall com ctx None "Native" "box_lrc" - - // unboxing obj back to a reference-typed record/union: downcast + re-wrap. - | Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef -> - let rawTy = transformEntityType com ctx entRef genArgs - let genArgsOpt = [ rawTy ] |> mkTypesGenericArgs - [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox_lrc" - // casts to generic param | _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value) @@ -1926,13 +1849,9 @@ module Util = // bindings we must emit a valid placeholder value instead (it is only there to // satisfy definite-assignment and gets overwritten before it is ever read). match typ with - | Fable.Any - | Fable.MetaType -> + | Fable.Any -> // `dyn Any` is unsized, so `Native::null` (needs a sized `NullableRef`) does // not apply; use a valid boxed-unit placeholder of type `LrcPtr`. - // `MetaType` (System.Type) is now also represented as `LrcPtr`, - // so it must take this path too (the `_` branch would emit a null of an - // unsized `dyn Any`, which does not compile). makeLibCall com ctx None "Native" "getZeroObj" [] | _ -> // Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref @@ -2141,84 +2060,10 @@ module Util = let fmt = makeFormatString parts makeFormatExpr com ctx fmt values - // Builds a rich reflection value for `typeof`. The emitted expression - // registers the record's field metadata + constructor/getter closures keyed by - // its concrete TypeId and returns a boxed RecordTypeInfo (the runtime value that - // a `System.Type` holds on the Rust target). This backs FSharpValue.MakeRecord, - // FSharpValue.GetRecordFields/GetRecordField and FSharpType.GetRecordFields. - let makeRecordTypeInfo (com: IRustCompiler) ctx r (entRef: Fable.EntityRef) genArgs (typ: Fable.Type) : Rust.Expr = - let ent = com.GetEntity(entRef) - let idents = getEntityFieldsAsIdents com ent - let objType = Fable.Any - let arrObjType = Fable.Array(objType, Fable.MutableArray) - - // tid = Reflection::type_id::() (concrete, unwrapped struct type) - let rawTy = transformEntityType com ctx entRef genArgs - let tidGenArgs = [ rawTy ] |> mkTypesGenericArgs - let tidExpr = makeLibCall com ctx tidGenArgs "Reflection" "type_id" [] - - // name - let nameExpr = makeStrConst ent.FullName |> transformExpr com ctx - - // field names: string[] - let fieldNamesExpr = - let names = idents |> List.map (fun ident -> makeStrConst ident.Name) - - Fable.Value(Fable.NewArray(Fable.ArrayValues names, Fable.String, Fable.MutableArray), None) - |> transformExpr com ctx - - // make: |vs: obj[]| box (Foo { X = unbox vs.[0]; Y = unbox vs.[1] }) - let makeExpr = - let vsIdent = makeTypedIdent arrObjType "vs" - - let ctorValues = - idents - |> List.mapi (fun i ident -> - let elem = - Fable.Get(Fable.IdentExpr vsIdent, Fable.ExprGet(makeIntConst i), objType, None) - - Fable.TypeCast(elem, ident.Type) // unbox to field type - ) - - let recordExpr = Fable.Value(Fable.NewRecord(ctorValues, entRef, genArgs), None) - let body = Fable.TypeCast(recordExpr, objType) // box the record - - Fable.Delegate([ vsIdent ], body, None, Fable.Tags.empty) - |> transformExpr com ctx - - // getters: (obj -> obj)[], each |o| box ((unbox o).Field) - let gettersExpr = - let getterFnType = Fable.DelegateType([ objType ], objType) - - let getters = - idents - |> List.map (fun ident -> - let oIdent = makeTypedIdent objType "o" - let recExpr = Fable.TypeCast(Fable.IdentExpr oIdent, typ) // unbox obj -> Foo - let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type) - let fieldGet = Fable.Get(recExpr, fieldInfo, ident.Type, None) - let body = Fable.TypeCast(fieldGet, objType) // box the field value - Fable.Delegate([ oIdent ], body, None, Fable.Tags.empty) - ) - - Fable.Value(Fable.NewArray(Fable.ArrayValues getters, getterFnType, Fable.MutableArray), None) - |> transformExpr com ctx - - makeLibCall com ctx None "Reflection" "recordType" [ tidExpr; nameExpr; fieldNamesExpr; makeExpr; gettersExpr ] - let makeTypeInfo (com: IRustCompiler) ctx r (typ: Fable.Type) : Rust.Expr = - match typ with - | Fable.DeclaredType(entRef, genArgs) when (com.GetEntity(entRef)).IsFSharpRecord -> - makeRecordTypeInfo com ctx r entRef genArgs typ - | _ -> - // Non-record `System.Type` value: carry the concrete `TypeId`, boxed as - // `LrcPtr` so it flows through the same `obj` slot as record type - // infos. Emitting a bare `Reflection_::TypeId::` path is not a valid - // value expression, and comparing the boxed carrier's runtime `type_id()` - // (as `typeEquals` did) would make all non-record types compare equal. - let genArgsOpt = transformGenArgs com ctx [ typ ] - let tidExpr = makeLibCall com ctx genArgsOpt "Reflection" "type_id" [] - makeLibCall com ctx None "Native" "box_" [ tidExpr ] + let importName = getLibraryImportName com ctx "Reflection" "TypeId" + let genArgsOpt = transformGenArgs com ctx [ typ ] + makeFullNamePathExpr importName genArgsOpt let transformValue (com: IRustCompiler) (ctx: Context) r value : Rust.Expr = let unimplemented () = @@ -3867,10 +3712,9 @@ module Util = mkUnitExpr () - | Fable.Quote(body, _isTyped, _r) -> - // Lower the quoted body to runtime calls that build the quotation AST, - // then transform those like any other expression (mirrors JS/Python/Beam). - QuotationEmitter.emitQuotedExpr com body |> transformExpr com ctx + | Fable.Quote _ -> + addError com [] None "Quotations are not yet supported for Rust target" + mkUnitExpr () let rec tryFindEntryPoint (com: IRustCompiler) decl : string list option = match decl with @@ -5436,19 +5280,7 @@ module Util = let genParams = FSharp2Fable.Util.getGenParamTypes genArgs - let ctx = - // For object expressions the struct is parameterized by the actual - // instantiation gen args (from the enclosing scope), and member bodies - // reference those gen param names - not the interface's formal params. - // Using the interface's formal names here would fail to filter the type's - // gen params out of each member's own generics (emitting a spurious ``). - let scopedGenArgs = - if isObjectExpr then - genArgs |> FSharp2Fable.Util.getGenParamNames |> Set.ofList - else - getEntityGenParamNames ent - - { ctx with ScopedEntityGenArgs = scopedGenArgs } + let ctx = { ctx with ScopedEntityGenArgs = getEntityGenParamNames ent } let isIgnoredMember (memb: Fable.MemberFunctionOrValue) = ent.IsFSharpExceptionDeclaration // to filter out compiler-generated exception equality @@ -5535,15 +5367,7 @@ module Util = |> List.collect (fun ifcEntRef -> let ifcGenArgs = if isObjectExpr then - // The object expression's declared type gen args apply to its - // primary interface only. Inherited interfaces (e.g. IDisposable - // from IEnumerator<'T>) carry their own gen args, which may have a - // different arity - so blindly reusing genArgs would emit e.g. - // `IDisposable`. Look those up in the declared type's hierarchy. - if ifcEntRef.FullName = entRef.FullName then - genArgs - else - ifcEntRef |> findInterfaceGenArgs com ent + genArgs else ifcEntRef |> findInterfaceGenArgs com ent diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 604d1db1d3..0af218b01a 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -490,9 +490,8 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Array _ -> Helper.LibCall(com, "Array", "equals", t, [ left; right ], ?loc = r) | List _ -> Helper.LibCall(com, "List", "equals", t, [ left; right ], ?loc = r) | IEnumerable -> Helper.LibCall(com, "Seq", "equals", t, [ left; right ], ?loc = r) - // System.Type is erased to a boxed reflection object (dyn Any, no PartialEq), - // so type-identity equality is compared on the carried TypeId at runtime. - | MetaType -> Helper.LibCall(com, "Reflection", "typeEquals", t, [ left; right ], ?loc = r) + // | MetaType -> + // Helper.LibCall(com, "Reflection", "equals", t, [left; right], ?loc=r) | HasReferenceEquality com _ -> referenceEquals com ctx r left right | Nullable _ -> // transforms null checks into option tests @@ -1299,13 +1298,10 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> objectHash com ctx r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - // Dynamic type of a boxed value: resolve via the runtime reflection - // registry (populated by typeof). Returns the concrete type's info - // when registered, else the value as an opaque placeholder. - Helper.LibCall(com, "Reflection", "getTypeFromObj", t, [ arg ], ?loc = r) - |> Some - else - makeTypeInfo r arg.Type |> Some + "Types can only be resolved at compile time. At runtime this will be same as `typeof`" + |> addWarning com ctx.InlinePath r + + makeTypeInfo r arg.Type |> Some | _ -> None let valueTypes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2879,66 +2875,14 @@ let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti let systemEnv (com: ICompiler) (ctx: Context) - (r: SourceLocation option) - (t: Type) + (_: SourceLocation option) + (_: Type) (i: CallInfo) (_: Expr option) - (args: Expr list) + (_: Expr list) = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") - | "GetEnvironmentVariable" -> - Helper.LibCall(com, "Environment", "getEnvironmentVariable", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "get_CurrentDirectory" -> - Helper.LibCall(com, "Environment", "getCurrentDirectory", t, [], ?loc = r) - |> Some - | _ -> None - -let paths (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = - match i.CompiledName with - | "Combine" -> - match args with - | [ _; _ ] -> - Helper.LibCall(com, "Path", "combine2", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ _; _; _ ] -> - Helper.LibCall(com, "Path", "combine3", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | _ -> None - | "GetDirectoryName" -> - Helper.LibCall(com, "Path", "getDirectoryName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileName" -> - Helper.LibCall(com, "Path", "getFileName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileNameWithoutExtension" -> - Helper.LibCall(com, "Path", "getFileNameWithoutExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetExtension" -> - Helper.LibCall(com, "Path", "getExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "HasExtension" -> - Helper.LibCall(com, "Path", "hasExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetTempPath" -> Helper.LibCall(com, "Path", "getTempPath", t, [], ?loc = r) |> Some - | "GetRandomFileName" -> Helper.LibCall(com, "Path", "getRandomFileName", t, [], ?loc = r) |> Some - | _ -> None - -let files (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = - match i.CompiledName with - | "Exists" -> - Helper.LibCall(com, "File", "exists", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "WriteAllText" -> - Helper.LibCall(com, "File", "writeAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ReadAllText" -> - Helper.LibCall(com, "File", "readAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Delete" -> - Helper.LibCall(com, "File", "delete", t, args, i.SignatureArgTypes, ?loc = r) - |> Some | _ -> None // Initial support, making at least InvariantCulture compile-able @@ -3673,8 +3617,6 @@ let private replacedModules = Types.timespan, timeSpans "System.Timers.Timer", timers "System.Environment", systemEnv - "System.IO.File", files - "System.IO.Path", paths "System.Globalization.CultureInfo", globalization "System.Random", random "System.Threading.CancellationToken", cancels @@ -3753,34 +3695,13 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr fsharpType com methName r t info args else fsharpValue com methName r t info args - | "Microsoft.FSharp.Reflection.UnionCaseInfo" -> - // UnionCaseInfo from a NewUnionCase quotation deconstruction is a Rust-native - // FSharpUnionCaseInfo carrier; route member access to the quotation runtime. - match thisArg, info.CompiledName with - | Some c, "get_Name" -> Helper.LibCall(com, "quotation", "unionCaseName", t, [ c ], ?loc = r) |> Some - | Some c, "get_Tag" -> Helper.LibCall(com, "quotation", "unionCaseTag", t, [ c ], ?loc = r) |> Some - | _ -> None + | "Microsoft.FSharp.Reflection.UnionCaseInfo" | "System.Reflection.PropertyInfo" | "System.Reflection.ParameterInfo" | "System.Reflection.MethodBase" | "System.Reflection.MethodInfo" | "System.Reflection.MemberInfo" -> - // Name/DeclaringType are inherited from MemberInfo, so `mi.Name` / `mi.DeclaringType` - // on a Call quotation binding arrive here even though the binding's type is MethodInfo. - // Detect that carrier via the thisArg's Fable type and route to the quotation runtime: - // get_Name -> a dedicated accessor (not the generic reflection name()); DeclaringType - // -> the declaring-type fullname boxed as System.Type, so mi.DeclaringType.FullName works. - let isMethodInfoCarrier (c: Expr) = - match c.Type with - | DeclaredType(e, _) -> e.FullName = "System.Reflection.MethodInfo" - | _ -> false - match thisArg, info.CompiledName with - | Some c, "get_Name" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodName", t, [ c ], ?loc = r) |> Some - | Some c, "get_DeclaringType" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodDeclaringType", t, [ c ], ?loc = r) - |> Some | Some c, "get_Tag" -> makeStrConst "tag" |> getExpr r t c |> Some | Some c, "get_ReturnType" -> makeStrConst "returnType" |> getExpr r t c |> Some | Some c, "GetParameters" -> makeStrConst "parameters" |> getExpr r t c |> Some @@ -3793,12 +3714,9 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr match c with | Value(TypeInfo(exprType, _), loc) -> getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some - // Runtime PropertyInfo (e.g. a record field info): read its carried name. - // Distinct from the generic type-name helper `name()` to avoid a clash. - | c -> Helper.LibCall(com, "Reflection", "propertyName", t, [ c ], ?loc = r) |> Some + | c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some | _ -> None - // F# Quotations - | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName + | _ -> None let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with diff --git a/src/fable-library-rust/src/Async.rs b/src/fable-library-rust/src/Async.rs index bd539baeaa..b04f3eab4d 100644 --- a/src/fable-library-rust/src/Async.rs +++ b/src/fable-library-rust/src/Async.rs @@ -165,11 +165,8 @@ pub mod Monitor_ { LOCKS.get_or_init(|| RwLock::new(HashSet::new())) } - // `?Sized` so a boxed `obj` (LrcPtr) can be locked on: casting to - // `*const ()` first discards the vtable of a fat pointer, leaving the data - // address, which is what identifies the lock. - pub fn enter(o: LrcPtr) { - let p = Arc::::as_ptr(&o) as *const () as usize; + pub fn enter(o: LrcPtr) { + let p = Arc::::as_ptr(&o) as usize; loop { let otherHasLock = try_init_and_get_locks().read().unwrap().get(&p).is_some(); if otherHasLock { @@ -181,8 +178,8 @@ pub mod Monitor_ { } } - pub fn exit(o: LrcPtr) { - let p = Arc::::as_ptr(&o) as *const () as usize; + pub fn exit(o: LrcPtr) { + let p = Arc::::as_ptr(&o) as usize; let hasRemoved = try_init_and_get_locks().write().unwrap().remove(&p); if (!hasRemoved) { panic!("Not removed {}", p) diff --git a/src/fable-library-rust/src/Environment.rs b/src/fable-library-rust/src/Environment.rs deleted file mode 100644 index 645e17d0e1..0000000000 --- a/src/fable-library-rust/src/Environment.rs +++ /dev/null @@ -1,21 +0,0 @@ -#[cfg(not(feature = "no_std"))] -pub mod Environment_ { - use crate::Native_::NullableRef; - use crate::String_::{fromString, string}; - - // .NET's Environment.GetEnvironmentVariable returns null when the - // variable is not set, so we map a missing/invalid value to a null string. - pub fn getEnvironmentVariable(name: string) -> string { - match std::env::var(name.as_str()) { - Ok(value) => fromString(value), - Err(_) => ::null(), - } - } - - pub fn getCurrentDirectory() -> string { - match std::env::current_dir() { - Ok(path) => fromString(path.to_string_lossy().into_owned()), - Err(_) => string(""), - } - } -} diff --git a/src/fable-library-rust/src/Event.rs b/src/fable-library-rust/src/Event.rs deleted file mode 100644 index 7f873bfce6..0000000000 --- a/src/fable-library-rust/src/Event.rs +++ /dev/null @@ -1,293 +0,0 @@ -// F#-style events for Rust. Mirrors src/fable-library-ts/Event.ts and -// src/fable-library-py/fable_library/event.py. -// -// The Microsoft.FSharp.Control event *interfaces* (IDelegateEvent`1, IEvent`2) -// live in Interfaces.fs so they take part in Fable's namespace aggregation. -// The concrete runtime types (FSharpEvent`1, AnonymousEvent) live here in the -// `Event_` module; a transformType mapping in the Rust backend points the -// FSharpEvent`1 type at `Event_::FSharpEvent`1`. - -pub mod Event_ { - use crate::Choice_::Choice_2; - use crate::Microsoft::FSharp::Control::{IDelegateEvent_1, IEvent_2}; - use crate::Native_::{ - getZeroObj, referenceEquals, refCell, Any, Func0, Func1, Func2, LrcPtr, MutCell, Vec, - }; - use crate::Observable_::{mkDisposable, mkObserver, noCompleted, noError}; - use crate::System::{IDisposable, IObservable_1, IObserver_1}; - - // Handler<'T> = delegate of obj * 'T -> unit - pub type Handler = Func2, T, ()>; - - // ----- AnonymousEvent: the published IEvent ----- - // A concrete type so the Publish field can be used through IEvent_2 - // (AddHandler/RemoveHandler) *and* unsize-coerced to `dyn IObservable_1` - // (in F#/JS an IEvent is also an IObservable). - #[derive(Clone)] - pub struct AnonymousEvent { - add_handler: Func1, ()>, - remove_handler: Func1, ()>, - } - - impl core::fmt::Debug for AnonymousEvent { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl core::fmt::Display for AnonymousEvent { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl AnonymousEvent { - fn do_sub(&self, observer: LrcPtr>) -> LrcPtr { - let h: Handler = - Func2::new(move |_sender: LrcPtr, args: T| observer.OnNext(args)); - (self.add_handler)(h.clone()); - let rem = self.remove_handler.clone(); - mkDisposable(Func0::new(move || rem(h.clone()))) - } - - // Inherent methods so calls on the concrete `Publish` field resolve - // without the interface trait having to be imported at the call site - // (Fable does not always emit a `use` for the flattened event trait). - pub fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { - self.do_sub(observer) - } - pub fn AddHandler(&self, handler: Handler) { - (self.add_handler)(handler) - } - pub fn RemoveHandler(&self, handler: Handler) { - (self.remove_handler)(handler) - } - } - - impl IEvent_2, T> for AnonymousEvent { - fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { - self.do_sub(observer) - } - fn AddHandler(&self, handler: Handler) { - (self.add_handler)(handler) - } - fn RemoveHandler(&self, handler: Handler) { - (self.remove_handler)(handler) - } - } - - impl IDelegateEvent_1> for AnonymousEvent { - fn AddHandler(&self, handler: Handler) { - (self.add_handler)(handler) - } - fn RemoveHandler(&self, handler: Handler) { - (self.remove_handler)(handler) - } - } - - impl IObservable_1 for AnonymousEvent { - fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { - self.do_sub(observer) - } - } - - // ----- FSharpEvent`1 ----- - #[derive(Clone)] - pub struct FSharpEvent_1 { - handlers: LrcPtr>>>, - pub Publish: LrcPtr>, - } - - impl core::fmt::Debug for FSharpEvent_1 { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl core::fmt::Display for FSharpEvent_1 { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl FSharpEvent_1 { - pub fn Trigger(&self, value: T) { - // Snapshot so a handler may unsubscribe during dispatch. - let hs = self.handlers.get().clone(); - for h in hs.iter() { - h(getZeroObj(), value.clone()); - } - } - } - - // Constructor for `new Event<'T>()` (routed here from the events replacement). - pub fn default() -> LrcPtr> { - let handlers: LrcPtr>>> = refCell(Vec::new()); - let add_handler = Func1::new({ - let handlers = handlers.clone(); - move |h: Handler| handlers.get_mut().push(h) - }); - let remove_handler = Func1::new({ - let handlers = handlers.clone(); - move |h: Handler| { - let vec = handlers.get_mut(); - // Compare by underlying function identity (the shared `dyn Fn` - // pointer). Func2's own PartialEq compares wrapper-handle - // addresses, which differ between clones, and the handler removed - // is a clone of the one stored. - if let Some(pos) = vec.iter().position(|x| referenceEquals(&**x, &*h)) { - vec.remove(pos); - } - } - }); - let publish = LrcPtr::new(AnonymousEvent { - add_handler, - remove_handler, - }); - LrcPtr::new(FSharpEvent_1 { - handlers, - Publish: publish, - }) - } - - // `CreateEvent`: build an IEvent from add/remove handler callbacks. - pub fn createEvent( - add_handler: Func1, ()>, - remove_handler: Func1, ()>, - ) -> LrcPtr> { - LrcPtr::new(AnonymousEvent { - add_handler, - remove_handler, - }) - } - - // ----- module functions ----- - - pub fn add( - callback: Func1, - source: LrcPtr, T>>, - ) { - source.Subscribe(mkObserver(callback, noError(), noCompleted())); - } - - pub fn choose( - chooser: Func1>, - source: LrcPtr, T>>, - ) -> LrcPtr, U>> { - let ev = default::(); - let evc = ev.clone(); - add( - Func1::new(move |t: T| { - if let Some(u) = chooser(t) { - evc.Trigger(u); - } - }), - source, - ); - ev.Publish.clone() - } - - pub fn filter( - predicate: Func1, - source: LrcPtr, T>>, - ) -> LrcPtr, T>> { - choose( - Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), - source, - ) - } - - pub fn map( - mapping: Func1, - source: LrcPtr, T>>, - ) -> LrcPtr, U>> { - let ev = default::(); - let evc = ev.clone(); - add(Func1::new(move |t: T| evc.Trigger(mapping(t))), source); - ev.Publish.clone() - } - - pub fn merge( - event1: LrcPtr, T>>, - event2: LrcPtr, T>>, - ) -> LrcPtr, T>> { - let ev = default::(); - let ev1 = ev.clone(); - let ev2 = ev.clone(); - add(Func1::new(move |t: T| ev1.Trigger(t)), event1); - add(Func1::new(move |t: T| ev2.Trigger(t)), event2); - ev.Publish.clone() - } - - pub fn pairwise( - source: LrcPtr, T>>, - ) -> LrcPtr>, LrcPtr<(T, T)>>> { - let ev = default::>(); - let evc = ev.clone(); - let last = refCell::>(None); - add( - Func1::new(move |next: T| { - if let Some(l) = last.get().clone() { - evc.Trigger(LrcPtr::new((l, next.clone()))); - } - last.set(Some(next)); - }), - source, - ); - ev.Publish.clone() - } - - pub fn partition( - predicate: Func1, - source: LrcPtr, T>>, - ) -> LrcPtr<( - LrcPtr, T>>, - LrcPtr, T>>, - )> { - let pred = predicate.clone(); - let first = filter(predicate, source.clone()); - let second = filter(Func1::new(move |x: T| !pred(x)), source); - LrcPtr::new((first, second)) - } - - pub fn scan( - collector: Func2, - state: U, - source: LrcPtr, T>>, - ) -> LrcPtr, U>> { - let st = refCell(state); - map( - Func1::new(move |t: T| { - let u = collector(st.get().clone(), t); - st.set(u.clone()); - u - }), - source, - ) - } - - pub fn split( - splitter: Func1>>, - source: LrcPtr, T>>, - ) -> LrcPtr<( - LrcPtr, U1>>, - LrcPtr, U2>>, - )> { - let sp = splitter.clone(); - let first = choose( - Func1::new(move |v: T| match &*sp(v) { - Choice_2::Choice1Of2(x) => Some(x.clone()), - _ => None, - }), - source.clone(), - ); - let second = choose( - Func1::new(move |v: T| match &*splitter(v) { - Choice_2::Choice2Of2(x) => Some(x.clone()), - _ => None, - }), - source, - ); - LrcPtr::new((first, second)) - } -} diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 30ccc1242e..0dffee4111 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -27,8 +27,6 @@ - - diff --git a/src/fable-library-rust/src/File.rs b/src/fable-library-rust/src/File.rs deleted file mode 100644 index acdcfc42bb..0000000000 --- a/src/fable-library-rust/src/File.rs +++ /dev/null @@ -1,28 +0,0 @@ -#[cfg(not(feature = "no_std"))] -pub mod File_ { - use crate::String_::{fromString, string}; - - pub fn exists(path: string) -> bool { - std::path::Path::new(path.as_str()).exists() - } - - pub fn writeAllText(path: string, contents: string) { - std::fs::write(path.as_str(), contents.as_str()) - .unwrap_or_else(|e| panic!("Could not write file '{}': {}", path.as_str(), e)) - } - - pub fn readAllText(path: string) -> string { - let contents = std::fs::read_to_string(path.as_str()) - .unwrap_or_else(|e| panic!("Could not read file '{}': {}", path.as_str(), e)); - fromString(contents) - } - - pub fn delete(path: string) { - // .NET File.Delete is a no-op when the file does not exist. - match std::fs::remove_file(path.as_str()) { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => panic!("Could not delete file '{}': {}", path.as_str(), e), - } - } -} diff --git a/src/fable-library-rust/src/Interfaces.fs b/src/fable-library-rust/src/Interfaces.fs index 54893b52fc..6974b5700a 100644 --- a/src/fable-library-rust/src/Interfaces.fs +++ b/src/fable-library-rust/src/Interfaces.fs @@ -3,14 +3,6 @@ namespace System type IDisposable = abstract Dispose: unit -> unit -type IObserver<'T> = - abstract OnNext: 'T -> unit - abstract OnError: exn -> unit - abstract OnCompleted: unit -> unit - -type IObservable<'T> = - abstract Subscribe: IObserver<'T> -> IDisposable - type IEquatable<'T> = abstract Equals: 'T -> bool @@ -87,19 +79,3 @@ type IDictionary<'K, 'V> = abstract ContainsKey: 'K -> bool abstract TryGetValue: 'K * byref<'V> -> bool abstract Remove: 'K -> bool - - -namespace Microsoft.FSharp.Control - -type IDelegateEvent<'Delegate> = - abstract AddHandler: 'Delegate -> unit - abstract RemoveHandler: 'Delegate -> unit - -// NOTE: the second type parameter is named 'T (not 'Args) on purpose: when the -// Rust backend flattens the inherited System.IObservable<_>.Subscribe method into -// this trait it keeps IObservable's original parameter name ('T) verbatim instead -// of substituting the actual argument, so the names must line up or the generated -// trait references an undeclared `T`. -type IEvent<'Delegate, 'T> = - inherit IDelegateEvent<'Delegate> - inherit System.IObservable<'T> diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index a6c2383003..42a249daf3 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -704,48 +704,6 @@ pub mod Native_ { try_downcast::<_, T>(&o).unwrap().clone() } - // Boxing/unboxing for reference-typed (Lrc-wrapped) declared types such as - // records and unions. Unlike `box_`, which wraps the value in a fresh Lrc, - // these coerce the *same* smart pointer to `dyn Any`, so the boxed value's - // concrete pointee is the record/union struct itself. That lets a plain - // downcast recover it, and lets `TypeId::of::()` (computed at the typeof - // site) match the pointee's runtime type id for value-based reflection. - #[cfg(not(feature = "lrc_ptr"))] - pub fn box_lrc(o: LrcPtr) -> LrcPtr { - o - } - - // With the `lrc_ptr` feature LrcPtr is a wrapper without CoerceUnsized, so - // coerce the inner Rc/Arc (which has it) and re-wrap. Same allocation, so - // pointer identity and the pointee's runtime type id are both preserved. - #[cfg(feature = "lrc_ptr")] - pub fn box_lrc(o: LrcPtr) -> LrcPtr { - let inner: Lrc = Lrc::clone(&*o); - let coerced: Lrc = inner; - LrcPtr::from(coerced) - } - - pub fn unbox_lrc(o: LrcPtr) -> LrcPtr { - #[cfg(not(feature = "lrc_ptr"))] - let inner: Lrc = o; - #[cfg(feature = "lrc_ptr")] - let inner: Lrc = Lrc::clone(&*o); - - // Downcast the smart pointer itself rather than cloning the value, so - // box_lrc + unbox_lrc round-trips pointer identity (ReferenceEquals). - // SAFETY: the `is::` check guarantees the allocation's pointee is a T; - // casting the fat pointer to *const T only drops the vtable. This is what - // Rc::downcast does internally, done manually because Arc only offers a - // stable downcast for Send + Sync pointees. - if (*inner).is::() { - let raw = Lrc::into_raw(inner) as *const T; - let typed = unsafe { Lrc::from_raw(raw) }; - fromFluent(typed) - } else { - panic!("Invalid unbox") - } - } - pub fn ofObj(value: T) -> Option { if is_null(&value) { None:: diff --git a/src/fable-library-rust/src/Observable.rs b/src/fable-library-rust/src/Observable.rs deleted file mode 100644 index 826370874e..0000000000 --- a/src/fable-library-rust/src/Observable.rs +++ /dev/null @@ -1,375 +0,0 @@ -pub mod Observable_ { - use crate::Choice_::Choice_2; - use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr}; - use crate::System::{Exception, IDisposable, IObservable_1, IObserver_1}; - - // ----------------------------------------------------------- - // Observer - // ----------------------------------------------------------- - - #[derive(Clone)] - pub struct Observer { - on_next: Func1, - on_error: Func1, ()>, - on_completed: Func0<()>, - } - - impl core::fmt::Debug for Observer { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl core::fmt::Display for Observer { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl IObserver_1 for Observer { - fn OnNext(&self, arg0: T) { - (self.on_next)(arg0) - } - fn OnError(&self, arg0: LrcPtr) { - (self.on_error)(arg0) - } - fn OnCompleted(&self) { - (self.on_completed)() - } - } - - // ----------------------------------------------------------- - // Observable - // ----------------------------------------------------------- - - #[derive(Clone)] - pub struct Observable { - subscribe: Func1>, LrcPtr>, - } - - impl core::fmt::Debug for Observable { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl core::fmt::Display for Observable { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl IObservable_1 for Observable { - fn Subscribe(&self, arg0: LrcPtr>) -> LrcPtr { - (self.subscribe)(arg0) - } - } - - // ----------------------------------------------------------- - // Disposable - // ----------------------------------------------------------- - - #[derive(Clone)] - pub struct Disposable { - dispose: Func0<()>, - } - - impl core::fmt::Debug for Disposable { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl core::fmt::Display for Disposable { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", core::any::type_name::()) - } - } - - impl IDisposable for Disposable { - fn Dispose(&self) { - (self.dispose)() - } - } - - // ----------------------------------------------------------- - // Constructors - // ----------------------------------------------------------- - - pub fn mkObserver( - on_next: Func1, - on_error: Func1, ()>, - on_completed: Func0<()>, - ) -> LrcPtr> { - interface_cast!( - LrcPtr::new(Observer { - on_next, - on_error, - on_completed, - }), - Lrc>, - ) - } - - fn mkObservable( - subscribe: Func1>, LrcPtr>, - ) -> LrcPtr> { - interface_cast!( - LrcPtr::new(Observable { subscribe }), - Lrc>, - ) - } - - pub fn mkDisposable(dispose: Func0<()>) -> LrcPtr { - interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc,) - } - - // A no-op error handler (errors are propagated by combinators when present, - // but the default subscriber ignores them, mirroring the JS/TS reference). - pub fn noError() -> Func1, ()> { - Func1::new(|_e: LrcPtr| ()) - } - - pub fn noCompleted() -> Func0<()> { - Func0::new(|| ()) - } - - // Build an error handler that forwards to a downstream observer. - fn fwdError( - observer: LrcPtr>, - ) -> Func1, ()> { - Func1::new(move |e: LrcPtr| observer.OnError(e)) - } - - // Build a completion handler that forwards to a downstream observer. - fn fwdCompleted(observer: LrcPtr>) -> Func0<()> { - Func0::new(move || observer.OnCompleted()) - } - - // ----------------------------------------------------------- - // Combinators - // ----------------------------------------------------------- - - pub fn add(callback: Func1, source: LrcPtr>) { - source.Subscribe(mkObserver(callback, noError(), noCompleted())); - } - - pub fn subscribe( - callback: Func1, - source: LrcPtr>, - ) -> LrcPtr { - source.Subscribe(mkObserver(callback, noError(), noCompleted())) - } - - pub fn choose( - chooser: Func1>, - source: LrcPtr>, - ) -> LrcPtr> { - mkObservable(Func1::new( - move |observer: LrcPtr>| -> LrcPtr { - let chooser = chooser.clone(); - let obs = observer.clone(); - let inner = mkObserver( - Func1::new(move |t: T| { - if let Some(u) = chooser(t) { - obs.OnNext(u); - } - }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), - ); - source.Subscribe(inner) - }, - )) - } - - pub fn filter( - predicate: Func1, - source: LrcPtr>, - ) -> LrcPtr> { - choose( - Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), - source, - ) - } - - pub fn map( - mapping: Func1, - source: LrcPtr>, - ) -> LrcPtr> { - mkObservable(Func1::new( - move |observer: LrcPtr>| -> LrcPtr { - let mapping = mapping.clone(); - let obs = observer.clone(); - let inner = mkObserver( - Func1::new(move |t: T| obs.OnNext(mapping(t))), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), - ); - source.Subscribe(inner) - }, - )) - } - - pub fn merge( - source1: LrcPtr>, - source2: LrcPtr>, - ) -> LrcPtr> { - mkObservable(Func1::new( - move |observer: LrcPtr>| -> LrcPtr { - let stopped = refCell(false); - let completed1 = refCell(false); - let completed2 = refCell(false); - - let h1 = { - let stopped = stopped.clone(); - let completed1 = completed1.clone(); - let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); - let obs_c = observer.clone(); - let se = stopped.clone(); - let sc = stopped.clone(); - source1.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), - Func0::new(move || { - if !*sc.get() { - completed1.set(true); - if *completed2.get() { - sc.set(true); - obs_c.OnCompleted(); - } - } - }), - )) - }; - - let h2 = { - let stopped = stopped.clone(); - let completed1 = completed1.clone(); - let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); - let obs_c = observer.clone(); - let se = stopped.clone(); - let sc = stopped.clone(); - source2.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), - Func0::new(move || { - if !*sc.get() { - completed2.set(true); - if *completed1.get() { - sc.set(true); - obs_c.OnCompleted(); - } - } - }), - )) - }; - - mkDisposable(Func0::new(move || { - h1.Dispose(); - h2.Dispose(); - })) - }, - )) - } - - pub fn pairwise( - source: LrcPtr>, - ) -> LrcPtr>> { - mkObservable(Func1::new( - move |observer: LrcPtr>>| -> LrcPtr { - let last = refCell::>(None); - let obs = observer.clone(); - let inner = mkObserver( - Func1::new(move |next: T| { - if let Some(l) = last.get().clone() { - obs.OnNext(LrcPtr::new((l, next.clone()))); - } - last.set(Some(next)); - }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), - ); - source.Subscribe(inner) - }, - )) - } - - pub fn partition( - predicate: Func1, - source: LrcPtr>, - ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { - let pred = predicate.clone(); - let first = filter(predicate, source.clone()); - let second = filter(Func1::new(move |x: T| !pred(x)), source); - LrcPtr::new((first, second)) - } - - pub fn scan( - collector: Func2, - state: U, - source: LrcPtr>, - ) -> LrcPtr> { - mkObservable(Func1::new( - move |observer: LrcPtr>| -> LrcPtr { - let collector = collector.clone(); - let st = refCell(state.clone()); - let obs = observer.clone(); - let inner = mkObserver( - Func1::new(move |t: T| { - let u = collector(st.get().clone(), t); - st.set(u.clone()); - obs.OnNext(u); - }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), - ); - source.Subscribe(inner) - }, - )) - } - - pub fn split( - splitter: Func1>>, - source: LrcPtr>, - ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { - let sp = splitter.clone(); - let first = choose( - Func1::new(move |v: T| match &*sp(v) { - Choice_2::Choice1Of2(x) => Some(x.clone()), - _ => None, - }), - source.clone(), - ); - let second = choose( - Func1::new(move |v: T| match &*splitter(v) { - Choice_2::Choice2Of2(x) => Some(x.clone()), - _ => None, - }), - source, - ); - LrcPtr::new((first, second)) - } -} diff --git a/src/fable-library-rust/src/Path.rs b/src/fable-library-rust/src/Path.rs deleted file mode 100644 index d40f9d547e..0000000000 --- a/src/fable-library-rust/src/Path.rs +++ /dev/null @@ -1,121 +0,0 @@ -#[cfg(not(feature = "no_std"))] -pub mod Path_ { - use crate::String_::{fromString, string}; - - fn is_sep(c: char) -> bool { - c == '/' || c == '\\' - } - - // Index just after the last path separator, or 0 if none. - fn file_name_start(path: &str) -> usize { - match path.rfind(is_sep) { - Some(i) => i + 1, - None => 0, - } - } - - // .NET/JS-style: returns the substring after the last separator. - pub fn getFileName(path: string) -> string { - let s = path.as_str(); - fromString(s[file_name_start(s)..].to_string()) - } - - // Returns the directory portion (everything before the last separator), - // or an empty string when there is no separator. Trailing separators on - // the directory portion are trimmed, matching .NET semantics. - pub fn getDirectoryName(path: string) -> string { - let s = path.as_str(); - match s.rfind(is_sep) { - Some(i) => { - let dir = &s[..i]; - let dir = dir.trim_end_matches(is_sep); - fromString(dir.to_string()) - } - None => string(""), - } - } - - // Returns the extension including the leading dot, or an empty string. - // Matches Node's extname/.NET: a leading dot in the file name is not an - // extension (e.g. ".gitignore" has no extension). - pub fn getExtension(path: string) -> string { - let s = path.as_str(); - let name = &s[file_name_start(s)..]; - match name.rfind('.') { - Some(i) if i > 0 => fromString(name[i..].to_string()), - _ => string(""), - } - } - - pub fn getFileNameWithoutExtension(path: string) -> string { - let s = path.as_str(); - let name = &s[file_name_start(s)..]; - let stem = match name.rfind('.') { - Some(i) if i > 0 => &name[..i], - _ => name, - }; - fromString(stem.to_string()) - } - - pub fn hasExtension(path: string) -> bool { - let s = path.as_str(); - let name = &s[file_name_start(s)..]; - matches!(name.rfind('.'), Some(i) if i > 0) - } - - fn combine_str(a: &str, b: &str) -> String { - if a.is_empty() { - return b.to_string(); - } - if b.is_empty() { - return a.to_string(); - } - // .NET semantics: a rooted later argument discards the earlier one, - // e.g. Path.Combine("/foo", "/bar") == "/bar". - if b.starts_with(is_sep) { - return b.to_string(); - } - let a = a.trim_end_matches(is_sep); - format!("{}/{}", a, b) - } - - pub fn combine2(path1: string, path2: string) -> string { - fromString(combine_str(path1.as_str(), path2.as_str())) - } - - pub fn combine3(path1: string, path2: string, path3: string) -> string { - let ab = combine_str(path1.as_str(), path2.as_str()); - fromString(combine_str(&ab, path3.as_str())) - } - - pub fn getTempPath() -> string { - fromString(std::env::temp_dir().to_string_lossy().into_owned()) - } - - // Non-cryptographic randomness derived from the system clock and an - // atomic counter; good enough for unique temp file names. - fn rand_u64() -> u64 { - use core::sync::atomic::{AtomicU64, Ordering}; - static COUNTER: AtomicU64 = AtomicU64::new(0); - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0); - let c = COUNTER.fetch_add(1, Ordering::Relaxed); - let mut x = nanos ^ c.wrapping_mul(0x9E37_79B9_7F4A_7C15); - x ^= x >> 30; - x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); - x ^= x >> 27; - x = x.wrapping_mul(0x94D0_49BB_1331_11EB); - x ^= x >> 31; - x - } - - // Mirrors .NET's Path.GetRandomFileName(): 8 random hex chars, a dot, - // then 3 more random hex chars. - pub fn getRandomFileName() -> string { - let name = (rand_u64() as u32) & 0xFFFF_FFFF; - let ext = (rand_u64() as u32) & 0xFFF; - fromString(format!("{:08x}.{:03x}", name, ext)) - } -} diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs deleted file mode 100644 index f10f2e517c..0000000000 --- a/src/fable-library-rust/src/Quotation.fs +++ /dev/null @@ -1,319 +0,0 @@ -// F# quotation runtime for the Fable Rust target (functions). -// -// Authored in F# and transpiled by Fable so the FSharpExpr/FSharpVar types land at the -// right namespace and every mk*/is* signature matches what generated user code expects. -// The module is named `quotation_` to match the `libModule = "quotation"` import path. -module quotation_ - -open Microsoft.FSharp.Quotations - -// --- Var constructor + accessors --- - -let mkQuotVar (name: string) (typ: string) (isMutable: bool) : FSharpVar = - { - Name = name - Type = typ - IsMutable = isMutable - } - -let varGetName (v: FSharpVar) : string = v.Name -let varGetType (v: FSharpVar) : string = v.Type -let varGetIsMutable (v: FSharpVar) : bool = v.IsMutable - -// --- Expr constructors --- - -// Generic so callers can pass an unboxed literal (i32/bool/string/...); `box` erases -// it to obj. A non-generic obj parameter would require the caller to box, which the -// emitter does not do on statically typed targets. -let mkValue<'T> (value: 'T) (typ: string) : FSharpExpr = ExprValue(box value, typ) - -// A null-value node (no instance / null literal / empty option or list). -// The payload is a boxed sentinel (not a null literal, which the Rust target can't -// represent for an untyped obj); consumers distinguish null nodes by the type tag. -let mkNull (typ: string) : FSharpExpr = ExprValue(box 0, typ) - -let mkVar (v: FSharpVar) : FSharpExpr = ExprVarExpr v -let mkLambda (v: FSharpVar) (body: FSharpExpr) : FSharpExpr = ExprLambda(v, body) -let mkApplication (func: FSharpExpr) (arg: FSharpExpr) : FSharpExpr = ExprApplication(func, arg) -let mkLet (v: FSharpVar) (value: FSharpExpr) (body: FSharpExpr) : FSharpExpr = ExprLet(v, value, body) - -let mkIfThenElse (guard: FSharpExpr) (thenExpr: FSharpExpr) (elseExpr: FSharpExpr) : FSharpExpr = - ExprIfThenElse(guard, thenExpr, elseExpr) - -let mkCall (instance: FSharpExpr) (method: string) (args: FSharpExpr[]) (declaringType: string) : FSharpExpr = - ExprCall(instance, method, args, declaringType) - -let mkSequential (first: FSharpExpr) (second: FSharpExpr) : FSharpExpr = ExprSequential(first, second) - -// A correctly-typed empty argument array. Rust is statically typed, so an empty -// obj[] built by the emitter won't unify with the FSharpExpr[] parameters of -// mkCall/mkNewUnion/mkNewTuple (zero-arg calls like property getters, nullary -// union cases). The emitter routes empty expr-arrays here for the Rust target. -let emptyExprArray () : FSharpExpr[] = [||] - -let mkNewTuple (elements: FSharpExpr[]) : FSharpExpr = ExprNewTuple elements - -let mkNewUnion (typeName: string) (tag: int) (caseName: string) (fields: FSharpExpr[]) : FSharpExpr = - ExprNewUnion(typeName, tag, caseName, fields) - -let mkNewRecord (typeName: string) (fieldNames: string[]) (values: FSharpExpr[]) : FSharpExpr = - ExprNewRecord(typeName, fieldNames, values) - -// --- UnionCaseInfo accessors (backing uci.Name / uci.Tag on a NewUnionCase binding) --- - -let unionCaseName (u: FSharpUnionCaseInfo) : string = u.Name -let unionCaseTag (u: FSharpUnionCaseInfo) : int = u.Tag - -// --- MethodInfo accessors (backing mi.Name / mi.DeclaringType on a Call binding) --- - -// mi.Name -> string (dedicated accessor; distinct from the generic reflection name()). -let methodName (m: FSharpMethodInfo) : string = m.Name - -// mi.DeclaringType -> System.Type. Rust has no faithful System.Type runtime, so the -// declaring type is represented by its fullname string boxed as obj (System.Type erases -// to Any on Rust). mi.DeclaringType.FullName then routes to Reflection.fullName, which -// unboxes the string. -let methodDeclaringType (m: FSharpMethodInfo) : obj = box m.DeclaringType -let mkNewList (head: FSharpExpr) (tail: FSharpExpr) : FSharpExpr = ExprNewList(head, tail) -let mkTupleGet (e: FSharpExpr) (index: int) : FSharpExpr = ExprTupleGet(e, index) -let mkUnionTag (e: FSharpExpr) : FSharpExpr = ExprUnionTag e -let mkUnionField (e: FSharpExpr) (fieldIndex: int) : FSharpExpr = ExprUnionField(e, fieldIndex) -let mkFieldGet (e: FSharpExpr) (fieldName: string) : FSharpExpr = ExprFieldGet(e, fieldName) -let mkFieldSet (e: FSharpExpr) (fieldName: string) (value: FSharpExpr) : FSharpExpr = ExprFieldSet(e, fieldName, value) -let mkVarSet (target: FSharpExpr) (value: FSharpExpr) : FSharpExpr = ExprVarSet(target, value) - -// --- Type accessor --- - -let getType (e: FSharpExpr) : string = - match e with - | ExprValue(_, t) -> t - | ExprLambda(v, _) -> v.Type - | _ -> "obj" - -// --- Pattern helpers (return option, following the active-pattern convention) --- - -let isValue (e: FSharpExpr) = - match e with - | ExprValue(v, t) -> Some(v, t) - | _ -> None - -let isVar (e: FSharpExpr) = - match e with - | ExprVarExpr v -> Some v - | _ -> None - -let isLambda (e: FSharpExpr) = - match e with - | ExprLambda(v, b) -> Some(v, b) - | _ -> None - -let isApplication (e: FSharpExpr) = - match e with - | ExprApplication(f, a) -> Some(f, a) - | _ -> None - -let isLet (e: FSharpExpr) = - match e with - | ExprLet(v, value, body) -> Some(v, value, body) - | _ -> None - -let isIfThenElse (e: FSharpExpr) = - match e with - | ExprIfThenElse(g, t, el) -> Some(g, t, el) - | _ -> None - -let isCall (e: FSharpExpr) = - match e with - | ExprCall(instance, m, args, dt) -> - // A static/operator call carries the "novalue" node as its instance; - // expose it as None so Patterns.Call matches F#. The tag is distinct from - // "null", which is a genuine quoted null value. - let inst = - match instance with - | ExprValue(_, "novalue") -> None - | _ -> Some instance - - // Build the MethodInfo carrier from the stored compiled-name + declaring-type - // fullname, so the Call binding exposes the real F# shape (mi.Name / - // mi.DeclaringType.FullName). This is the SQLProvider linchpin: the declaring - // type distinguishes List.map from Array.map. - let mi: FSharpMethodInfo = - { - Name = m - DeclaringType = dt - } - - Some(inst, mi, List.ofArray args) - | _ -> None - -let isSequential (e: FSharpExpr) = - match e with - | ExprSequential(f, s) -> Some(f, s) - | _ -> None - -let isNewTuple (e: FSharpExpr) = - match e with - | ExprNewTuple els -> Some(List.ofArray els) - | _ -> None - -let isNewUnionCase (e: FSharpExpr) = - match e with - | ExprNewUnion(typeName, tag, caseName, fields) -> - // Return F#'s (UnionCaseInfo * Expr list) shape. The UnionCaseInfo carrier - // backs uci.Name/uci.Tag via unionCaseName/unionCaseTag. - let uci: FSharpUnionCaseInfo = - { - Name = caseName - Tag = tag - DeclaringType = typeName - } - - Some(uci, List.ofArray fields) - | _ -> None - -let isNewRecord (e: FSharpExpr) = - match e with - | ExprNewRecord(typeName, _names, values) -> - // Return F#'s (Type * Expr list) shape. Rust has no faithful System.Type runtime, - // so the type slot is erased to a boxed value (the record type name). The Rust - // compiler erases the pattern's System.Type slot to Any to match (see Replacements). - Some(box typeName, List.ofArray values) - | _ -> None - -let isTupleGet (e: FSharpExpr) = - match e with - | ExprTupleGet(inner, i) -> Some(inner, i) - | _ -> None - -let isFieldGet (e: FSharpExpr) = - match e with - | ExprFieldGet(inner, n) -> - // Return F#'s (Expr option * PropertyInfo * Expr list) shape, as JS/TS/Python do. - // A static property get carries the "novalue" node as its target (distinct from - // "null", a genuine quoted null); expose it as None so Patterns.PropertyGet matches - // F#. The third slot mirrors PropertyGet's indexer args, always empty here. - let inst = - match inner with - | ExprValue(_, "novalue") -> None - | _ -> Some inner - - let pi: FSharpPropertyInfo = { Name = n } - Some(inst, pi, ([]: FSharpExpr list)) - | _ -> None - -// --- Free variables --- - -let getFreeVars (e: FSharpExpr) : FSharpVar list = - let seen = System.Collections.Generic.HashSet() - let acc = ResizeArray() - - let rec walk (bound: Set) (e: FSharpExpr) = - match e with - | ExprVarExpr v -> - if not (bound.Contains v.Name) && seen.Add v.Name then - acc.Add v - | ExprLambda(v, body) -> walk (bound.Add v.Name) body - | ExprLet(v, value, body) -> - walk bound value - walk (bound.Add v.Name) body - | ExprApplication(f, a) -> - walk bound f - walk bound a - | ExprIfThenElse(g, t, el) -> - walk bound g - walk bound t - walk bound el - | ExprCall(_, _, args, _) -> - for a in args do - walk bound a - | ExprSequential(f, s) -> - walk bound f - walk bound s - | ExprNewTuple els -> - for el in els do - walk bound el - | ExprTupleGet(inner, _) -> walk bound inner - | ExprFieldGet(inner, _) -> walk bound inner - | _ -> () - - walk Set.empty e - List.ofSeq acc - -// --- Evaluation (structural cases + common operators; SQL translation deconstructs -// rather than evaluates, so this covers the tested subset). --- - -let private applyOperator (method: string) (args: obj list) : obj = - // Extract args positionally rather than binding them in the match: pattern-binding - // obj (Rc) variables makes Fable zero-initialize a fat pointer, which is - // invalid at runtime. Matching only on the method string avoids that. - // if/elif (not match): Fable-Rust would emit string literals as match patterns, - // which isn't valid Rust; an if-chain compiles to string == comparisons. - let i (n: int) : int = unbox (List.item n args) - let b (n: int) : bool = unbox (List.item n args) - - if method = "op_Addition" then - box (i 0 + i 1) - elif method = "op_Subtraction" then - box (i 0 - i 1) - elif method = "op_Multiply" then - box (i 0 * i 1) - elif method = "op_Division" then - box (i 0 / i 1) - elif method = "op_Modulus" then - box (i 0 % i 1) - elif method = "op_UnaryNegation" then - box (-(i 0)) - elif method = "op_Equality" then - box (i 0 = i 1) - elif method = "op_Inequality" then - box (i 0 <> i 1) - elif method = "op_LessThan" then - box (i 0 < i 1) - elif method = "op_LessThanOrEqual" then - box (i 0 <= i 1) - elif method = "op_GreaterThan" then - box (i 0 > i 1) - elif method = "op_GreaterThanOrEqual" then - box (i 0 >= i 1) - elif method = "op_BooleanAnd" then - box (b 0 && b 1) - elif method = "op_BooleanOr" then - box (b 0 || b 1) - elif method = "op_LogicalNot" then - box (not (b 0)) - else - failwithf "Cannot evaluate method: %s" method - -let evaluate (e: FSharpExpr) : obj = - let rec eval (env: Map) (e: FSharpExpr) : obj = - match e with - | ExprValue(v, _) -> v - | ExprVarExpr v -> Map.find v.Name env - | ExprLambda(v, body) -> box (fun (arg: obj) -> eval (Map.add v.Name arg env) body) - | ExprApplication(f, a) -> - let func = unbox obj> (eval env f) - func (eval env a) - | ExprLet(v, value, body) -> eval (Map.add v.Name (eval env value) env) body - | ExprIfThenElse(g, t, el) -> - if unbox (eval env g) then - eval env t - else - eval env el - | ExprSequential(a, b) -> - eval env a |> ignore - eval env b - | ExprCall(_, method, args, _) -> applyOperator method [ for a in args -> eval env a ] - // A tuple literal evaluates to a boxed obj[] of its evaluated elements, mirroring - // PHP (which produces a plain array). TupleGet then indexes into that array; without - // this arm `eval inner` on a tuple literal would fall through to failwith and there - // would be nothing indexable, so both arms are needed together. - | ExprNewTuple els -> box [| for el in els -> eval env el |] - | ExprTupleGet(inner, index) -> - // The match-bound `index` is a borrowed &i32 on Rust while the array Index - // operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the - // arithmetic dereferences the borrow) so the indexing type-checks. - let i = index + 0 - (unbox (eval env inner)).[i] - | _ -> failwith "Cannot evaluate expression" - - eval Map.empty e diff --git a/src/fable-library-rust/src/QuotationTypes.fs b/src/fable-library-rust/src/QuotationTypes.fs deleted file mode 100644 index d853a33b26..0000000000 --- a/src/fable-library-rust/src/QuotationTypes.fs +++ /dev/null @@ -1,65 +0,0 @@ -// F# quotation AST types for the Fable Rust target. -// -// Declared in the Microsoft.FSharp.Quotations namespace with the *compiled* names -// (FSharpExpr / FSharpVar) that Fable maps the real FSharp.Core Quotations.Expr / .Var -// to on the Rust target. Using the compiled names (rather than Expr/Var) means no clash -// with FSharp.Core during F# type-checking, while still resolving to the same Rust path -// (Microsoft::FSharp::Quotations::FSharpExpr) that generated user code references. -namespace Microsoft.FSharp.Quotations - -type FSharpVar = - { - Name: string - Type: string - IsMutable: bool - } - -// Rust-native carrier for Microsoft.FSharp.Reflection.UnionCaseInfo. Fable-Rust -// erases the F# UnionCaseInfo type to this struct (see Fable2Rust.transformType), -// so `uci.Name`/`uci.Tag` on a NewUnionCase binding route to the accessors below. -type FSharpUnionCaseInfo = - { - Name: string - Tag: int - DeclaringType: string - } - -// Rust-native carrier for System.Reflection.MethodInfo. Fable-Rust erases the F# -// MethodInfo type to this struct (see Fable2Rust.transformType), so `mi.Name` and -// `mi.DeclaringType` on a Call quotation deconstruction route to the accessors in -// Quotation.fs. DeclaringType is stored as the declaring-type fullname string (the -// emitter fills it from declaringEntity.FullName, e.g. "...Collections.ListModule"). -type FSharpMethodInfo = - { - Name: string - DeclaringType: string - } - -// Rust-native carrier for System.Reflection.PropertyInfo. Fable-Rust erases the F# -// PropertyInfo type to this struct (see Fable2Rust.transformType), so it is the single -// carrier for both quotation deconstruction (PropertyGet's propInfo) and reflection -// (FSharpType.GetRecordFields), matching .NET where both yield the same PropertyInfo. -// It carries only the name, as a quotation knows nothing else about the property: the -// field getter is resolved from the reflection registry by name when a value is read, -// mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. -type FSharpPropertyInfo = { Name: string } - -type FSharpExpr = - | ExprValue of value: obj * typ: string - | ExprVarExpr of var: FSharpVar - | ExprLambda of var: FSharpVar * body: FSharpExpr - | ExprApplication of func: FSharpExpr * arg: FSharpExpr - | ExprLet of var: FSharpVar * value: FSharpExpr * body: FSharpExpr - | ExprIfThenElse of guard: FSharpExpr * thenExpr: FSharpExpr * elseExpr: FSharpExpr - | ExprCall of instance: FSharpExpr * method: string * args: FSharpExpr[] * declaringType: string - | ExprSequential of first: FSharpExpr * second: FSharpExpr - | ExprNewTuple of elements: FSharpExpr[] - | ExprNewUnion of typeName: string * tag: int * caseName: string * fields: FSharpExpr[] - | ExprNewRecord of typeName: string * fieldNames: string[] * values: FSharpExpr[] - | ExprNewList of head: FSharpExpr * tail: FSharpExpr - | ExprTupleGet of expr: FSharpExpr * index: int - | ExprUnionTag of expr: FSharpExpr - | ExprUnionField of expr: FSharpExpr * fieldIndex: int - | ExprFieldGet of expr: FSharpExpr * fieldName: string - | ExprFieldSet of expr: FSharpExpr * fieldName: string * value: FSharpExpr - | ExprVarSet of target: FSharpExpr * value: FSharpExpr diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 713b62ecc2..6a26cf05ca 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -1,237 +1,11 @@ pub mod Reflection_ { pub use core::any::TypeId; // re-export - use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; - use crate::Native_::{box_, Any, Func1, LrcPtr, Vec}; - use crate::NativeArray_::{array_from, Array}; + //use crate::Native_::{LrcPtr}; use crate::String_::string; - #[cfg(not(feature = "no_std"))] - use core::cell::RefCell; - #[cfg(not(feature = "no_std"))] - use std::collections::HashMap; - pub fn name() -> string { // TODO: map some common type names to .NET type names string(core::any::type_name::()) } - - // The object (System.Object / obj) representation on the Rust target. - type obj = LrcPtr; - - // Compile-time helper emitted by `typeof` to obtain a concrete type id. - pub fn type_id() -> TypeId { - TypeId::of::() - } - - // PropertyInfo-like carrier for a single record field. - // `get` downcasts the boxed record and reads the field, returning it boxed. - #[derive(Clone)] - pub struct RecordFieldInfo { - pub name: string, - pub get: Func1, - } - - // Rich reflection info attached to `typeof`. Carries everything - // needed for MakeRecord (the `make` constructor closure) as well as - // GetRecordFields/GetRecordField (field names + per-field getters). - // This mirrors the JS/TS model where the reflected Type is an object - // carrying field metadata, so the same F# reflection code behaves the same. - #[derive(Clone)] - pub struct RecordTypeInfo { - pub tid: TypeId, - pub name: string, - pub fields: Array>, - pub make: Func1, obj>, - } - - // Registry mapping a record's *concrete* TypeId to its reflection info. - // Populated when `typeof` is evaluated. This is what makes - // value-based reflection (GetRecordFields(record)) possible: from a bare - // boxed record we can recover its runtime TypeId and look the info up. - // - // `thread_local!` is std-only, so under `no_std` there is no registry and - // the value-first entry points degrade (like exception catching does). - // Type-first reflection (typeof, MakeRecord, GetRecordElements) is - // unaffected, as those carry the info in the `System.Type` value itself. - #[cfg(not(feature = "no_std"))] - thread_local! { - static RECORD_REGISTRY: RefCell> = - RefCell::new(HashMap::new()); - } - - #[cfg(not(feature = "no_std"))] - fn registry_insert(tid: TypeId, info: RecordTypeInfo) { - RECORD_REGISTRY.with(|r| { - r.borrow_mut().insert(tid, info); - }); - } - - #[cfg(feature = "no_std")] - fn registry_insert(_tid: TypeId, _info: RecordTypeInfo) { - // no registry when no_std - } - - #[cfg(not(feature = "no_std"))] - fn registry_get(tid: &TypeId) -> Option { - RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) - } - - #[cfg(feature = "no_std")] - fn registry_get(_tid: &TypeId) -> Option { - None // no registry when no_std - } - - // Emitted by generated `typeof`. Builds the field metadata, registers - // the type by its concrete TypeId, and returns the boxed RecordTypeInfo - // (which is what a `System.Type` value holds on the Rust target). - pub fn recordType( - tid: TypeId, - name: string, - field_names: Array, - make: Func1, obj>, - getters: Array>, - ) -> obj { - let names: Vec = field_names.get().iter().cloned().collect(); - let gets: Vec> = getters.get().iter().cloned().collect(); - let fields_vec: Vec> = names - .into_iter() - .zip(gets.into_iter()) - .map(|(n, g)| LrcPtr::new(RecordFieldInfo { name: n, get: g })) - .collect(); - let info = RecordTypeInfo { - tid, - name, - fields: array_from(fields_vec), - make, - }; - registry_insert(tid, info.clone()); - box_(info) - } - - fn type_info_of(typ: &obj) -> RecordTypeInfo { - (**typ) - .downcast_ref::() - .expect("Type does not carry record reflection info") - .clone() - } - - // FSharpValue.MakeRecord(typ, values, ?bindingFlags) -> obj - pub fn makeRecord(typ: obj, values: Array, _flags: Option) -> obj { - let info = type_info_of(&typ); - (info.make)(values) - } - - // Resolves a field getter for a record value by property name, via the registry. - // The PropertyInfo carrier holds only a name (a quotation knows nothing more), so the - // getter is looked up here, mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. - fn getterOf(record: &obj, name: &string) -> Func1 { - let tid = (**record).type_id(); - let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); - let field = info - .fields - .get() - .iter() - .find(|f| &f.name == name) - .cloned() - .expect("Property not found on record type"); - field.get.clone() - } - - // FSharpType.GetRecordFields(typ, ?bindingFlags) -> PropertyInfo[] - pub fn getRecordElements(typ: obj, _flags: Option) -> Array> { - let info = type_info_of(&typ); - let props: Vec> = info - .fields - .get() - .iter() - .map(|f| LrcPtr::new(FSharpPropertyInfo { Name: f.name.clone() })) - .collect(); - array_from(props) - } - - // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] - pub fn getRecordFields(record: obj, _flags: Option) -> Array { - let tid = (*record).type_id(); - let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); - let vals: Vec = info - .fields - .get() - .iter() - .map(|f| (f.get)(record.clone())) - .collect(); - array_from(vals) - } - - // FSharpValue.GetRecordField(record, propInfo) -> obj - pub fn getRecordField(record: obj, info: LrcPtr) -> obj { - let get = getterOf(&record, &info.Name); - get(record) - } - - // PropertyInfo.Name -> string (dedicated accessor; distinct from name()). - pub fn propertyName(info: LrcPtr) -> string { - info.Name.clone() - } - - // PropertyInfo.GetValue(record) -> obj - pub fn getValue(info: LrcPtr, record: obj) -> obj { - let get = getterOf(&record, &info.Name); - get(record) - } - - // FSharpType.IsRecord(typ) -> bool - pub fn isRecord(typ: obj, _flags: Option) -> bool { - (*typ).downcast_ref::().is_some() - } - - // System.Type.FullName. On Rust a `System.Type` value is a boxed carrier: a record - // type carries RecordTypeInfo (use its registered name), while a quotation-derived - // declaring type (from MethodInfo.DeclaringType) carries the boxed fullname string. - pub fn fullName(typ: obj) -> string { - if let Some(info) = (*typ).downcast_ref::() { - info.name.clone() - } else if let Some(s) = (*typ).downcast_ref::() { - s.clone() - } else { - crate::String_::string("") - } - } - - // obj.GetType() for a statically-erased value (static type = obj/Any). - // Reads the boxed value's *runtime* type id and returns the concrete type's - // registered reflection info, so `(box record : obj).GetType()` resolves to - // the real record type (not typeof) whenever that type was registered - // via a prior `typeof`. Falls back to returning the object itself as an - // opaque placeholder for unregistered types. - pub fn getTypeFromObj(o: obj) -> obj { - let tid = (*o).type_id(); - match registry_get(&tid) { - Some(info) => box_(info), - None => o, - } - } - - // System.Type equality (typeof = typeof). Compares the carried concrete - // TypeId when both operands are record type infos; otherwise falls back to - // comparing the boxed values' runtime type ids. - pub fn typeEquals(a: obj, b: obj) -> bool { - let ta = (*a).downcast_ref::(); - let tb = (*b).downcast_ref::(); - match (ta, tb) { - (Some(x), Some(y)) => x.tid == y.tid, - (None, None) => { - // Non-record `System.Type` values carry a boxed concrete `TypeId`. - // Downcast both and compare the *carried* ids; comparing the boxed - // carriers' runtime `type_id()` would always be `TypeId::of::()` - // and therefore make every non-record type compare equal. - match ((*a).downcast_ref::(), (*b).downcast_ref::()) { - (Some(x), Some(y)) => x == y, - _ => (*a).type_id() == (*b).type_id(), - } - } - // One record, one non-record: definitely different types. - _ => false, - } - } } diff --git a/src/fable-library-rust/src/lib.fs b/src/fable-library-rust/src/lib.fs index 464b245a8a..d207dc8467 100644 --- a/src/fable-library-rust/src/lib.fs +++ b/src/fable-library-rust/src/lib.fs @@ -14,19 +14,14 @@ let _imports () = importAll "./Decimal.rs" importAll "./Format.rs" importAll "./Encoding.rs" - importAll "./Environment.rs" - importAll "./Event.rs" importAll "./Exception.rs" - importAll "./File.rs" importAll "./Guid.rs" importAll "./HashMap.rs" importAll "./HashSet.rs" importAll "./Interop.rs" importAll "./Native.rs" importAll "./NativeArray.rs" - importAll "./Observable.rs" importAll "./Numeric.rs" - importAll "./Path.rs" importAll "./Random.rs" importAll "./Reflection.rs" importAll "./RegExp.rs" diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 7264061dca..5af3a4faef 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -42,10 +42,9 @@ - + - - + @@ -59,12 +58,10 @@ - - + - @@ -74,7 +71,6 @@ - diff --git a/tests/Rust/tests/src/EnumerableTests.fs b/tests/Rust/tests/src/EnumerableTests.fs index 170c5998a6..e990afcc62 100644 --- a/tests/Rust/tests/src/EnumerableTests.fs +++ b/tests/Rust/tests/src/EnumerableTests.fs @@ -25,17 +25,17 @@ let fibGen() = let mutable state = (1,1) { new IEnumerator<_> with member x.Current = (fst state) - interface IEnumerator with + interface IEnumerator with member x.Current = box (fst state) member x.MoveNext() = state <- (snd state, fst state + snd state); (fst state) < 1000 member x.Reset() = state <- (1,1) - interface System.IDisposable with + interface System.IDisposable with member x.Dispose() = () } let toSeq gen = { new IEnumerable<'T> with member x.GetEnumerator() = gen() - interface IEnumerable with + interface IEnumerable with member x.GetEnumerator() = (gen() :> IEnumerator) } let fib() = Seq.unfold (fun state -> diff --git a/tests/Rust/tests/src/EnvironmentTests.fs b/tests/Rust/tests/src/EnvironmentTests.fs deleted file mode 100644 index 0d2437d45d..0000000000 --- a/tests/Rust/tests/src/EnvironmentTests.fs +++ /dev/null @@ -1,22 +0,0 @@ -// System.Environment reads process state, so the library module is std-only -// and these tests do not apply to no_std builds. -[] -module Fable.Tests.EnvironmentTests - -open Util.Testing -open System - -[] -let ``Environment.NewLine works`` () = - Environment.NewLine.Length > 0 - |> equal true - -[] -let ``Environment.GetEnvironmentVariable returns null for a missing variable`` () = - Environment.GetEnvironmentVariable("__FABLE_NON_EXISTENT_VAR__") - |> equal null - -[] -let ``Environment.CurrentDirectory works`` () = - Environment.CurrentDirectory.Length > 0 - |> equal true diff --git a/tests/Rust/tests/src/EventTests.fs b/tests/Rust/tests/src/EventTests.fs index dde1561419..8575bb83eb 100644 --- a/tests/Rust/tests/src/EventTests.fs +++ b/tests/Rust/tests/src/EventTests.fs @@ -2,44 +2,30 @@ module Fable.Tests.EventTests open Util.Testing -// NOTE (Rust): these class-hosted event types are disabled on Rust. They trigger -// with `event.Trigger(this, arg)`, i.e. they pass the enclosing instance (`this`) -// as an event value. The Rust backend cannot recover the reference-counted `this` -// from a `&self` method body (it emits a by-value struct clone of the wrong type), -// and `member Event = event.Publish` exposes the event as `IEvent<_>`, which the -// backend does not coerce to `IObservable<_>` for `.Add`/`.Subscribe`. The plain -// `Event<'T>` module-function tests below (the core of this port) are unaffected. -// These pass on the JS/TS/Python targets. -// type ClassWithCLIEvent() = -// let event = new Event<_>() -// [] -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// -// type ClassWithNonCLIEvent() = -// let event = new Event<_>() -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) - -// NOTE (Rust): the two-generic Event<'Delegate,'Args> (FSharpEvent`2, with a -// sender-carrying 2-arg Trigger) and CLI events declared on an *interface* are -// not supported by the Rust runtime port yet. The `Microsoft.FSharp.Control` -// runtime only implements FSharpEvent`1 (single generic, 1-arg Trigger). These -// types and the two tests that use them (below) are therefore disabled on Rust; -// they pass on the JS/TS/Python targets. -// type InterfaceWithCLIEvent<'t> = -// [] -// abstract Event : IEvent,'t> -// -// type ClassWithInterfaceWithCLIEvent<'t>() = -// let event = new Event<_,_>() -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// interface InterfaceWithCLIEvent<'t> with -// [] -// member _.Event = event.Publish +type ClassWithCLIEvent() = + let event = new Event<_>() + [] + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type ClassWithNonCLIEvent() = + let event = new Event<_>() + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type InterfaceWithCLIEvent<'t> = + [] + abstract Event : IEvent,'t> + +type ClassWithInterfaceWithCLIEvent<'t>() = + let event = new Event<_,_>() + member this.TestEvent(arg) = + event.Trigger(this, arg) + interface InterfaceWithCLIEvent<'t> with + [] + member _.Event = event.Publish module tests = @@ -210,61 +196,54 @@ module tests = source.Trigger 6 equal 0 result - // NOTE (Rust): disabled — see the ClassWithCLIEvent / ClassWithNonCLIEvent - // note above (passing `this` as an event value + IEvent->IObservable coercion - // for `.Add`/`.Subscribe`). These pass on JS/TS/Python. - // [] - // let ``Classes can trigger CLI events`` () = - // let mutable result = 0 - // let classWithEvent = new ClassWithCLIEvent() - // classWithEvent.Event.Add(fun (sender, x) -> result <- x) - // classWithEvent.TestEvent(5) - // equal 5 result - // - // [] - // let ``Classes can trigger non-CLI events`` () = - // let mutable result = "" - // let classWithEvent = new ClassWithNonCLIEvent() - // let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) - // classWithEvent.TestEvent("Hello") - // disp.Dispose() - // classWithEvent.TestEvent("Bye") - // equal "Hello" result - - // NOTE (Rust): disabled — needs the two-generic Event<_,_> (FSharpEvent`2, - // 2-arg Trigger with sender) and CLI events declared on interfaces, which the - // Rust runtime port does not implement yet (see the commented types above). - // These pass on JS/TS/Python. - // [] - // let ``Classes can trigger CLI events on interfaces`` () = - // let mutable result = 0 - // let mutable sender = null - // let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() - // (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> - // sender <- s - // result <- x - // ) - // classWithEvent.TestEvent(5) - // equal 5 result - // equal (box classWithEvent) sender - // - // [] - // let ``Generic interface expression can have CLI events`` () = - // let mutable actualSender = "" - // let mutable result = false - // let event = Event<_,_>() - // let ifaceWIthEvent = - // { new InterfaceWithCLIEvent<_> with - // [] - // member _.Event = event.Publish } - // ifaceWIthEvent.Event.AddHandler(fun sender arg -> - // actualSender <- string sender - // result <- arg) - // let expectedSender = "SENDER" - // let expectedResult = true - // event.Trigger(expectedSender, expectedResult) - // equal expectedSender actualSender - // equal expectedResult result + [] + let ``Classes can trigger CLI events`` () = + let mutable result = 0 + let classWithEvent = new ClassWithCLIEvent() + classWithEvent.Event.Add(fun (sender, x) -> result <- x) + classWithEvent.TestEvent(5) + equal 5 result + + [] + let ``Classes can trigger non-CLI events`` () = + let mutable result = "" + let classWithEvent = new ClassWithNonCLIEvent() + let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) + classWithEvent.TestEvent("Hello") + disp.Dispose() + classWithEvent.TestEvent("Bye") + equal "Hello" result + + [] + let ``Classes can trigger CLI events on interfaces`` () = + let mutable result = 0 + let mutable sender = null + let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() + (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> + sender <- s + result <- x + ) + classWithEvent.TestEvent(5) + equal 5 result + equal (box classWithEvent) sender + + [] + let ``Generic interface expression can have CLI events`` () = + let mutable actualSender = "" + let mutable result = false + let event = Event<_,_>() + let ifaceWIthEvent = + { new InterfaceWithCLIEvent<_> with + [] + member _.Event = event.Publish } + ifaceWIthEvent.Event.AddHandler(fun sender arg -> + actualSender <- string sender + result <- arg) + let expectedSender = "SENDER" + let expectedResult = true + event.Trigger(expectedSender, expectedResult) + equal expectedSender actualSender + equal expectedResult result [] let ``Events are unsubscribed correctly`` () = // See #609 diff --git a/tests/Rust/tests/src/ObservableTests.fs b/tests/Rust/tests/src/ObservableTests.fs index 0ed72d768e..45b641f5cd 100644 --- a/tests/Rust/tests/src/ObservableTests.fs +++ b/tests/Rust/tests/src/ObservableTests.fs @@ -9,35 +9,16 @@ type MyObserver<'T>(f) = member x.OnError e = () member x.OnCompleted() = () -// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for two -// Rust-backend limitations (the runtime combinators themselves are unaffected): -// 1. The disposable is a named generic class, not an inline -// `{ new IDisposable with ... }` object expression: the Rust backend does not -// propagate the enclosing type's generic parameter into a generated -// object-expression struct (it emits an inner struct that references `T` -// without declaring it -> rustc E0401). -// 2. Subscriptions are keyed by an int id and removed by id, rather than -// `listeners.Remove(w)`: interface trait objects (`IObserver<'T>`) do not -// implement Rust `PartialEq`, so removal-by-value of an interface element is -// not supported. Behaviour is identical to the reference. -type private Unsubscriber<'T>(listeners: ResizeArray>, id: int) = - interface IDisposable with - member _.Dispose() = - let idx = listeners.FindIndex(fun (i, _) -> i = id) - if idx >= 0 then listeners.RemoveAt(idx) - type MyObservable<'T>() = - let listeners = ResizeArray>() - let mutable nextId = 0 + let listeners = ResizeArray>() member x.Trigger v = - for (_, lis) in listeners do + for lis in listeners do lis.OnNext v interface IObservable<'T> with member x.Subscribe w = - let id = nextId - nextId <- nextId + 1 - listeners.Add((id, w)) - new Unsubscriber<'T>(listeners, id) :> IDisposable + listeners.Add(w) + { new IDisposable with + member x.Dispose() = listeners.Remove(w) |> ignore } module tests = diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs deleted file mode 100644 index 353a081eba..0000000000 --- a/tests/Rust/tests/src/QuotationTests.fs +++ /dev/null @@ -1,356 +0,0 @@ -module Fable.Tests.QuotationTests - -open Util.Testing -open Microsoft.FSharp.Quotations -open Microsoft.FSharp.Quotations.Patterns -open Microsoft.FSharp.Linq.RuntimeHelpers - -// NOTE: quotations are deconstructed through an untyped `Expr` parameter (helpers below -// take `(e: Expr)`); matching a typed `Expr<'T>` binding directly is not yet supported on -// Rust (see quotation notes). Binding the boxed `obj` payload out of a Value node now works -// (the getZeroObj fix); the type element is bound as a wildcard since the runtime models it -// as a string rather than System.Type. Array-valued nodes now deconstruct: NewTuple exposes -// its elements, NewUnionCase binds a real UnionCaseInfo (uci.Name / uci.Tag) plus its args, and -// NewRecord binds its (erased) type slot plus its args. The NewRecord type slot and -// UnionCaseInfo.GetFields() are not modelled (System.Type / PropertyInfo have no Rust runtime). - -let private lambdaVarName (e: Expr) = - match e with - | Lambda(v, _body) -> v.Name - | _ -> "?" - -let private letVarName (e: Expr) = - match e with - | Let(v, _value, _body) -> v.Name - | _ -> "?" - -let private isIfThenElseNode (e: Expr) = - match e with - | IfThenElse(_g, _t, _e) -> true - | _ -> false - -[] -let ``Evaluate a value`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ 42 @> - unbox r |> equal 42 - -// Typed Expr<'T> matched directly (not routed through an (e: Expr) helper): the type -// argument is erased so the quotation shares the untyped FSharpExpr representation. - -[] -let ``Typed quotation matches Value directly`` () = - match <@ 42 @> with - | Value(v, _) -> unbox v |> equal 42 - | _ -> failwith "Expected Value" - -[] -let ``Typed quotation matches Lambda directly`` () = - match <@ fun (x: int) -> x + 1 @> with - | Lambda(v, _) -> v.Name |> equal "x" - | _ -> failwith "Expected Lambda" - -[] -let ``Lambda quotation exposes the parameter name`` () = - lambdaVarName <@ fun (x: int) -> x + 1 @> |> equal "x" - -[] -let ``Let quotation exposes the bound name`` () = - letVarName <@ let y = 5 in y + 1 @> |> equal "y" - -[] -let ``IfThenElse quotation deconstructs`` () = - isIfThenElseNode <@ if true then 1 else 2 @> |> equal true - -[] -let ``Evaluate addition`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ 2 + 3 @> - unbox r |> equal 5 - -[] -let ``Evaluate subtraction`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ 10 - 4 @> - unbox r |> equal 6 - -[] -let ``Evaluate comparison`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> - unbox r |> equal true - -[] -let ``Evaluate let binding`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ let x = 10 in x + 5 @> - unbox r |> equal 15 - -[] -let ``Evaluate lambda application`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ (fun a -> a * 2) 21 @> - unbox r |> equal 42 - -[] -let ``Evaluate let-bound lambda`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ let f = (fun x -> x + 1) in f 41 @> - unbox r |> equal 42 - -// --- Parity expansion (toward the Python 27) --- -// Binding the boxed obj payload out of a Value node previously tripped getZero on Rust; -// the getZeroObj / active-pattern fixes should now allow these. Type element bound as -// wildcard (`Value(v, _)`) to sidestep the System.Type-vs-string model difference. - -let private valueInt (e: Expr) = - match e with - | Value(v, _) -> unbox v - | _ -> -1 - -let private valueBool (e: Expr) = - match e with - | Value(v, _) -> unbox v - | _ -> false - -let private valueStr (e: Expr) = - match e with - | Value(v, _) -> unbox v - | _ -> "?" - -let private letBoundInt (e: Expr) = - match e with - | Let(_, Value(v, _), _) -> unbox v - | _ -> -1 - -let private seqSecondInt (e: Expr) = - match e with - | Sequential(_, Value(v, _)) -> unbox v - | _ -> -1 - -let private newTupleLen (e: Expr) = - match e with - | NewTuple(exprs) -> List.length exprs - | _ -> -1 - -let private isAppLambdaValue (e: Expr) = - match e with - | Application(Lambda _, Value _) -> true - | _ -> false - -let private lambdaVarBodyName (e: Expr) = - match e with - | Lambda(v, Var v2) -> v.Name + "/" + v2.Name - | _ -> "?" - -let private isLambdaIfThenElse (e: Expr) = - match e with - | Lambda(_, IfThenElse(_, _, _)) -> true - | _ -> false - -[] -let ``Value node binds int payload`` () = - valueInt <@ 42 @> |> equal 42 - -[] -let ``Value node binds bool payload`` () = - valueBool <@ true @> |> equal true - -[] -let ``Value node binds string payload`` () = - valueStr <@ "hello" @> |> equal "hello" - -[] -let ``Let binds value node payload`` () = - letBoundInt <@ let x = 5 in x @> |> equal 5 - -[] -let ``Sequential exposes second value payload`` () = - seqSecondInt <@ (); 42 @> |> equal 42 - -[] -let ``NewTuple exposes element count`` () = - newTupleLen <@ (1, 2, 3) @> |> equal 3 - -[] -let ``Application of lambda to value deconstructs`` () = - isAppLambdaValue <@ (fun x -> x) 42 @> |> equal true - -[] -let ``Lambda body Var refers to the parameter`` () = - lambdaVarBodyName <@ fun x -> x @> |> equal "x/x" - -[] -let ``Option match in quotation lowers to IfThenElse`` () = - isLambdaIfThenElse <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> |> equal true - -[] -let ``Literal match in quotation lowers to IfThenElse`` () = - isLambdaIfThenElse <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> |> equal true - -[] -let ``Evaluate multiplication`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ 6 * 7 @> - unbox r |> equal 42 - -[] -let ``Evaluate nested let bindings`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ let x = 3 in let y = 4 in x * y @> - unbox r |> equal 12 - -[] -let ``Evaluate nested lambda`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> - unbox r |> equal 7 - -// --- NewUnionCase / NewRecord deconstruction --- - -type private MyRec = { X: int; Y: int } - -let private newUnionCaseName (e: Expr) = - match e with - | NewUnionCase(uci, _args) -> uci.Name - | _ -> "?" - -let private newUnionCaseArgCount (e: Expr) = - match e with - | NewUnionCase(_uci, args) -> List.length args - | _ -> -1 - -let private newUnionCaseTag (e: Expr) = - match e with - | NewUnionCase(uci, _args) -> uci.Tag - | _ -> -1 - -let private newRecordArgCount (e: Expr) = - match e with - | NewRecord(_t, args) -> List.length args - | _ -> -1 - -[] -let ``NewUnionCase exposes the case name`` () = - newUnionCaseName <@ Some 42 @> |> equal "Some" - -[] -let ``NewUnionCase exposes the argument count`` () = - newUnionCaseArgCount <@ Some 42 @> |> equal 1 - -[] -let ``NewUnionCase exposes the tag`` () = - newUnionCaseTag <@ Some 42 @> |> equal 1 - -[] -let ``NewRecord exposes the argument count`` () = - newRecordArgCount <@ ({ X = 1; Y = 2 }: MyRec) @> |> equal 2 - -// --- TupleGet evaluation (B3) --- -// A tuple-deconstructing let lowers to a NewTuple bound to a var, then TupleGet nodes -// pulling out each element. Evaluating it exercises both the ExprNewTuple arm (builds a -// boxed obj[]) and the ExprTupleGet arm (indexes into it). Before the fix, TupleGet -// returned the whole tuple, so `a`/`b` were the obj[] and `a + b` would not evaluate. - -[] -let ``Evaluate tuple-get picks the element at the index`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ let (a, b) = (10, 20) in a + b @> - unbox r |> equal 30 - -[] -let ``Evaluate tuple-get picks the second element`` () = - let r = LeafExpressionConverter.EvaluateQuotation <@ let (a, b, c) = (1, 2, 3) in b @> - unbox r |> equal 2 - -// --- Call deconstruction with MethodInfo (B1) --- -// Deconstructing a Call now binds a real MethodInfo: mi.Name is the compiled method name and -// mi.DeclaringType.FullName is the declaring module fullname. The declaring type is the -// SQLProvider linchpin: it distinguishes List.map from Array.map. MethodInfo erases to the -// Rust-native FSharpMethodInfo carrier; DeclaringType is the fullname string boxed as -// System.Type (System.Type erases to Any), read back via Reflection.fullName. - -let private callMethodName (e: Expr) = - match e with - | Call(_, mi, _) -> mi.Name - | _ -> "?" - -let private callDeclaringTypeName (e: Expr) = - match e with - | Call(_, mi, _) -> mi.DeclaringType.FullName - | _ -> "?" - -[] -let ``Call exposes the method name`` () = - callMethodName <@ List.map (fun x -> x + 1) [ 1 ] @> |> equal "Map" - -[] -let ``Call exposes the declaring type full name`` () = - callDeclaringTypeName <@ List.map (fun x -> x + 1) [ 1 ] @> - |> equal "Microsoft.FSharp.Collections.ListModule" - -[] -let ``Call distinguishes List.map from Array.map by declaring type`` () = - callDeclaringTypeName <@ Array.map (fun x -> x + 1) [| 1 |] @> - |> equal "Microsoft.FSharp.Collections.ArrayModule" - -[] -let ``Call binds MethodInfo directly`` () = - match <@ List.map (fun x -> x + 1) [ 1 ] @> with - | Call(_, mi, _) -> - mi.Name |> equal "Map" - mi.DeclaringType.FullName |> equal "Microsoft.FSharp.Collections.ListModule" - | _ -> failwith "Expected Call" - -// --- PropertyGet (isFieldGet) --- -// A property getter deconstructs as PropertyGet(instance, propInfo, indexerArgs), matching -// real F# quotations and the JS/TS/Python runtimes. System.Reflection.PropertyInfo erases to -// the shared FSharpPropertyInfo carrier, so pi.Name works here exactly as it does for the -// PropertyInfo values returned by FSharpType.GetRecordFields. - -type QuotPropHolder = - static member Version = 42 - -[] -let ``Option.Value access inside a quotation is a PropertyGet`` () = - let q = <@ fun (o: int option) -> o.Value @> - - match q with - | Lambda(_, PropertyGet(Some _, _, [])) -> () - | _ -> failwith "Expected Lambda with PropertyGet body" - -[] -let ``List.Head access inside a quotation is a PropertyGet`` () = - let q = <@ fun (xs: int list) -> xs.Head @> - - match q with - | Lambda(_, PropertyGet(Some _, _, [])) -> () - | _ -> failwith "Expected Lambda with PropertyGet body" - -// A static property carries the "novalue" no-instance sentinel as its target, which must -// surface as None rather than Some sentinelNode. -[] -let ``Static property access inside a quotation is a PropertyGet with no instance`` () = - let q = <@ QuotPropHolder.Version @> - - match q with - | PropertyGet(None, _, []) -> () - | _ -> failwith "Expected PropertyGet with no instance" - -[] -let ``PropertyGet exposes the property name`` () = - let q = <@ fun (o: int option) -> o.Value @> - - match q with - | Lambda(_, PropertyGet(_, pi, _)) -> pi.Name |> equal "Value" - | _ -> failwith "Expected Lambda with PropertyGet body" - -// --- Call instance slot (isCall) --- -// A static call carries the "novalue" no-instance sentinel as its instance, which must -// surface as None rather than Some sentinelNode — the isCall counterpart of the static -// PropertyGet test above. The instance case proves the sentinel is not over-applied. - -[] -let ``Static call inside a quotation has no instance`` () = - let q = <@ List.length [ 1; 2; 3 ] @> - - match q with - | Call(None, _mi, args) -> equal 1 (List.length args) - | _ -> failwith "Expected a static Call with no instance" - -[] -let ``Instance call inside a quotation has an instance`` () = - let q = <@ fun (s: string) -> s.Trim() @> - - match q with - | Lambda(_, Call(Some _, _mi, _)) -> () - | _ -> failwith "Expected Lambda with an instance Call body" diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs deleted file mode 100644 index bfa7462820..0000000000 --- a/tests/Rust/tests/src/RecordReflectionTests.fs +++ /dev/null @@ -1,125 +0,0 @@ -// Value-based reflection needs the type registry, which is std-only -// (it uses thread_local), so these tests do not apply to no_std builds. -[] -module Fable.Tests.RecordReflectionTests - -open Util.Testing -open Microsoft.FSharp.Reflection - -// Record reflection MVP for the Rust target. Mirrors the record-focused cases of -// the (disabled) ReflectionTests.fs / JS Main ReflectionTests.fs. Value comparisons -// are done after unbox<'T> rather than on boxed `obj` directly, because Rust has no -// structural equality on `dyn Any` (see report). Behaviour is otherwise equivalent. - -type TestRecord = { String: string; Int: int } -type OtherRecord = { Z: int } - -type SampleUnion = - | CaseA - | CaseB of int - -[] -let ``typeof and System.Type equality on records`` () = - (typeof = typeof) |> equal true - (typeof = typeof) |> equal false - -[] -let ``typeof and System.Type equality on non-record types`` () = - // A non-record `typeof` must materialize as a valid runtime value (a boxed - // TypeId), and equality must distinguish types rather than always being equal. - let t = typeof - ignore t - (typeof = typeof) |> equal true - (typeof = typeof) |> equal false - (typeof = typeof) |> equal true - (typeof = typeof) |> equal false - // Mixed record / non-record must not compare equal. - (typeof = typeof) |> equal false - -[] -let ``FSharpType.IsRecord works`` () = - FSharpType.IsRecord(typeof) |> equal true - -[] -let ``FSharpType.GetRecordFields exposes PropertyInfo names`` () = - let fields = FSharpType.GetRecordFields(typeof) - fields.Length |> equal 2 - fields.[0].Name |> equal "String" - fields.[1].Name |> equal "Int" - -[] -let ``FSharpValue.GetRecordFields returns boxed field values`` () = - // Value-based reflection resolves the type via the runtime registry, which is - // populated when typeof is evaluated (as the JS record test also does first). - FSharpType.IsRecord(typeof) |> equal true - let record = { String = "a"; Int = 1 } - let values = FSharpValue.GetRecordFields(record) - values.Length |> equal 2 - unbox values.[0] |> equal "a" - unbox values.[1] |> equal 1 - -[] -let ``FSharpValue.GetRecordField reads an individual field`` () = - let record = { String = "a"; Int = 1 } - let fields = FSharpType.GetRecordFields(typeof) - unbox (FSharpValue.GetRecordField(record, fields.[0])) |> equal "a" - unbox (FSharpValue.GetRecordField(record, fields.[1])) |> equal 1 - -[] -let ``PropertyInfo.GetValue reads a field from a boxed record`` () = - let record = { String = "a"; Int = 1 } - let fields = FSharpType.GetRecordFields(typeof) - unbox (fields.[0].GetValue(box record)) |> equal "a" - unbox (fields.[1].GetValue(box record)) |> equal 1 - -[] -let ``FSharpValue.MakeRecord round-trips`` () = - let record = { String = "a"; Int = 1 } - let values = FSharpValue.GetRecordFields(record) - let rebuilt = unbox (FSharpValue.MakeRecord(typeof, values)) - rebuilt |> equal record - -[] -let ``FSharp.Reflection: Record (end to end)`` () = - let record = { String = "a"; Int = 1 } - let recordTypeFields = FSharpType.GetRecordFields(typeof) - let recordValueFields = FSharpValue.GetRecordFields(record) - - FSharpType.IsRecord(typeof) |> equal true - recordTypeFields.Length |> equal 2 - - // field names line up with values (verified per-slot, unboxed) - recordTypeFields.[0].Name |> equal "String" - unbox recordValueFields.[0] |> equal "a" - recordTypeFields.[1].Name |> equal "Int" - unbox recordValueFields.[1] |> equal 1 - - // GetRecordField matches GetRecordFields per slot - unbox (FSharpValue.GetRecordField(record, recordTypeFields.[0])) |> equal "a" - unbox (FSharpValue.GetRecordField(record, recordTypeFields.[1])) |> equal 1 - - // MakeRecord reconstructs an equal record - unbox (FSharpValue.MakeRecord(typeof, recordValueFields)) |> equal record - -[] -let ``obj.GetType round-trips to the concrete record type`` () = - let record = { String = "a"; Int = 1 } - FSharpType.IsRecord(typeof) |> equal true // ensure registration - ((box record).GetType() = typeof) |> equal true - let rebuilt = - unbox (FSharpValue.MakeRecord((box record).GetType(), FSharpValue.GetRecordFields(record))) - rebuilt |> equal record - -// box_lrc/unbox_lrc coerce the same smart pointer rather than cloning the value, -// so a record boxed to obj and unboxed again is physically the same instance. -[] -let ``Boxing a record to obj and unboxing preserves reference identity`` () = - let r = - { - String = "a" - Int = 1 - } - - let o: obj = box r - let r2 = unbox o - System.Object.ReferenceEquals(r, r2) |> equal true diff --git a/tests/Rust/tests/src/SystemIOTests.fs b/tests/Rust/tests/src/SystemIOTests.fs deleted file mode 100644 index 4fcfadb38c..0000000000 --- a/tests/Rust/tests/src/SystemIOTests.fs +++ /dev/null @@ -1,83 +0,0 @@ -// System.IO needs a filesystem, so the library modules are std-only and -// these tests do not apply to no_std builds. -[] -module Fable.Tests.SystemIOTests - -open Util.Testing -open System.IO - -[] -let ``Path.Combine works with two parts`` () = - let combined = Path.Combine("foo", "bar") - Path.GetFileName(combined) |> equal "bar" - Path.GetDirectoryName(combined) |> equal "foo" - -[] -let ``Path.Combine works with three parts`` () = - let combined = Path.Combine("foo", "bar", "baz.txt") - Path.GetFileName(combined) |> equal "baz.txt" - Path.GetDirectoryName(combined) |> equal (Path.Combine("foo", "bar")) - -[] -let ``Path.Combine with a rooted second part uses the second part`` () = - // .NET: a rooted later argument discards the earlier one. - Path.Combine("/foo", "/bar") |> equal "/bar" - Path.Combine("foo", "/bar") |> equal "/bar" - -[] -let ``Path.GetDirectoryName works`` () = - Path.GetDirectoryName("temp/test.txt") - |> equal "temp" - -[] -let ``Path.GetExtension works`` () = - Path.GetExtension("temp/test.txt") - |> equal ".txt" - -[] -let ``Path.GetFileName works`` () = - Path.GetFileName("temp/test.txt") - |> equal "test.txt" - -[] -let ``Path.GetFileNameWithoutExtension works`` () = - Path.GetFileNameWithoutExtension("temp/test.txt") - |> equal "test" - -[] -let ``Path.HasExtension works`` () = - Path.HasExtension("temp/test.txt") - |> equal true - - Path.HasExtension("temp/test") - |> equal false - -[] -let ``Path.GetTempPath works`` () = - (Path.GetTempPath()).Length > 0 - |> equal true - -[] -let ``Path.GetRandomFileName works`` () = - (Path.GetRandomFileName()).Length > 0 - |> equal true - -[] -let ``File round-trip works`` () = - let file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) - File.Exists(file) |> equal false - - File.WriteAllText(file, "Hello World") - File.Exists(file) |> equal true - - File.ReadAllText(file) - |> equal "Hello World" - - File.Delete(file) - File.Exists(file) |> equal false - -[] -let ``File.Delete on a missing file is a no-op`` () = - // .NET semantics: deleting a non-existent file does not throw. - File.Delete("definitely-missing-file-xyz.tmp") - true |> equal true From 8a551bc9b2433035fc704dffc639e5f031ca7593 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 25 Aug 2026 18:41:58 +0000 Subject: [PATCH 002/322] feat(rust): System.Uri (PR 7 of 7 Rust improvements) (#4790) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 205 ++++++- src/Fable.Transforms/Rust/Replacements.fs | 144 ++++- src/fable-library-rust/src/Async.rs | 11 +- src/fable-library-rust/src/Environment.rs | 21 + src/fable-library-rust/src/Event.rs | 293 ++++++++++ .../src/Fable.Library.Rust.fsproj | 2 + src/fable-library-rust/src/File.rs | 28 + src/fable-library-rust/src/Interfaces.fs | 24 + src/fable-library-rust/src/Native.rs | 42 ++ src/fable-library-rust/src/Observable.rs | 375 +++++++++++++ src/fable-library-rust/src/Path.rs | 121 +++++ src/fable-library-rust/src/Quotation.fs | 319 +++++++++++ src/fable-library-rust/src/QuotationTypes.fs | 65 +++ src/fable-library-rust/src/Reflection.rs | 228 +++++++- src/fable-library-rust/src/Uri.rs | 507 ++++++++++++++++++ src/fable-library-rust/src/lib.fs | 6 + tests/Rust/Fable.Tests.Rust.fsproj | 12 +- tests/Rust/tests/src/EnumerableTests.fs | 6 +- tests/Rust/tests/src/EnvironmentTests.fs | 22 + tests/Rust/tests/src/EventTests.fs | 165 +++--- tests/Rust/tests/src/ObservableTests.fs | 29 +- tests/Rust/tests/src/QuotationTests.fs | 356 ++++++++++++ tests/Rust/tests/src/RecordReflectionTests.fs | 125 +++++ tests/Rust/tests/src/SystemIOTests.fs | 83 +++ tests/Rust/tests/src/UriTests.fs | 57 ++ 25 files changed, 3122 insertions(+), 124 deletions(-) create mode 100644 src/fable-library-rust/src/Environment.rs create mode 100644 src/fable-library-rust/src/Event.rs create mode 100644 src/fable-library-rust/src/File.rs create mode 100644 src/fable-library-rust/src/Observable.rs create mode 100644 src/fable-library-rust/src/Path.rs create mode 100644 src/fable-library-rust/src/Quotation.fs create mode 100644 src/fable-library-rust/src/QuotationTypes.fs create mode 100644 src/fable-library-rust/src/Uri.rs create mode 100644 tests/Rust/tests/src/EnvironmentTests.fs create mode 100644 tests/Rust/tests/src/QuotationTests.fs create mode 100644 tests/Rust/tests/src/RecordReflectionTests.fs create mode 100644 tests/Rust/tests/src/SystemIOTests.fs diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 76c2f6bb33..015e7d81e7 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -619,6 +619,13 @@ module TypeInfo = -> true | _ -> false + // A reference-typed (non-struct) record or union. Such entities are Lrc-wrapped + // at the value level, so boxing/unboxing them to `obj` needs a smart-pointer + // coercion rather than the plain value-type box (see transformCast). + let isReferenceRecordOrUnion (com: IRustCompiler) (entRef: Fable.EntityRef) = + let ent = com.GetEntity(entRef) + (ent.IsFSharpRecord || ent.IsFSharpUnion) && not ent.IsValueType + // Checks whether the type needs a ref counted wrapper // such as Rc (or Arc in a multithreaded context) let shouldBeRefCountWrapped (com: IRustCompiler) ctx typ = @@ -632,12 +639,15 @@ module TypeInfo = // always not Rc-wrapped | Fable.Unit | Fable.Measure _ - | Fable.MetaType | Fable.Boolean | Fable.Char | Fable.Number _ -> None // should be Rc-wrapped + // MetaType (System.Type) is erased to Any and carries a boxed reflection + // object at runtime, so it must be Lrc-wrapped like Any (a bare `dyn Any` + // is unsized and cannot appear as a value/argument). + | Fable.MetaType | Fable.Any | Fable.Regex | Replacements.Util.Builtin(Replacements.Util.FSharpReference _) @@ -1014,6 +1024,10 @@ module TypeInfo = let entName = getLibraryImportName com ctx "Random" "Random" let genArgsOpt = transformGenArgs com ctx genArgs makeFullNamePathTy entName genArgsOpt + | _ when entRef.FullName = "System.Uri" -> + let entName = getLibraryImportName com ctx "Uri" "Uri" + let genArgsOpt = transformGenArgs com ctx genArgs + makeFullNamePathTy entName genArgsOpt | ent -> let entName = getEntityFullName com ctx entRef let genArgsOpt = transformGenArgs com ctx genArgs @@ -1128,7 +1142,11 @@ module TypeInfo = | Fable.Char -> primitiveType "char" | Fable.Boolean -> primitiveType "bool" | Fable.String -> transformStringType com ctx - | Fable.MetaType -> transformMetaType com ctx + // System.Type has no faithful runtime on Rust (reflection is a stub), so erase it + // to Any. This lets the NewRecord quotation deconstruction bind its type slot to the + // boxed value the quotation runtime returns; typeof-based reflection is unsupported + // regardless (see the disabled ReflectionTests). + | Fable.MetaType -> transformAnyType com ctx | Fable.Number(kind, _) -> transformNumberType com ctx kind | Fable.LambdaType(argType, returnType) -> let argTypes, returnType = ([ argType ], returnType) @@ -1163,6 +1181,15 @@ module TypeInfo = // implemented random type | Replacements.Util.IsEntity (Types.random) (_, []) -> transformImportType com ctx [] "Random" "Random" + // implemented uri type + | Replacements.Util.IsEntity ("System.Uri") (_, []) -> transformImportType com ctx [] "Uri" "Uri" + + // implemented event type (FSharpEvent`1); the module functions and + // the .ctor are routed to Event_ by the events replacement, so the + // concrete type must resolve to Event_::FSharpEvent`1 as well. + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`1") (_, [ t ]) -> + transformImportType com ctx [ t ] "Event" "FSharpEvent`1" + // implemented regex types | Replacements.Util.IsEntity (Types.regexMatch) (_, []) -> transformImportType com ctx [] "RegExp" "Match" | Replacements.Util.IsEntity (Types.regexGroup) (_, []) -> transformImportType com ctx [] "RegExp" "Group" @@ -1184,6 +1211,52 @@ module TypeInfo = // built-in types | Replacements.Util.Builtin kind -> transformBuiltinType com ctx typ kind + // Typed quotation Expr<'T>: erase the type argument so it shares the untyped + // FSharpExpr runtime representation. This lets a typed quotation (e.g. <@ 42 @>) + // be matched directly against the Patterns active patterns, which operate on + // the untyped Expr. + | Fable.DeclaredType(entRef, _) when entRef.FullName = Types.fsharpExprGeneric -> + transformEntityType com ctx { entRef with FullName = Types.fsharpExpr } [] + + // UnionCaseInfo (from a NewUnionCase quotation deconstruction) is erased to the + // Rust-native FSharpUnionCaseInfo carrier so uci.Name/uci.Tag resolve to the + // quotation runtime accessors. We build the import path directly (rather than via + // transformEntityType) because that carrier type does not exist in FSharp.Core, so + // com.GetEntity on the renamed ref would fail; getEntityFullName imports the type + // from fable_library_rust purely by FullName. + | Fable.DeclaredType(entRef, _) when entRef.FullName = "Microsoft.FSharp.Reflection.UnionCaseInfo" -> + let entName = + getEntityFullName + com + ctx + { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpUnionCaseInfo" } + + makeFullNamePathTy entName None + + // System.Reflection.MethodInfo (from a Call quotation deconstruction) is erased to + // the Rust-native FSharpMethodInfo carrier so mi.Name / mi.DeclaringType resolve to + // the quotation runtime accessors. Built directly by FullName (mirrors the + // UnionCaseInfo case) because that carrier type does not exist in FSharp.Core. + | Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.MethodInfo" -> + let entName = + getEntityFullName com ctx { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpMethodInfo" } + + makeFullNamePathTy entName None + + // System.Reflection.PropertyInfo is erased to the Rust-native FSharpPropertyInfo + // carrier, so p.Name / p.GetValue and FSharpValue.GetRecordField resolve to the + // reflection runtime. One carrier serves both reflection (GetRecordFields) and + // quotations (PropertyGet's propInfo), as in .NET. Built directly by FullName + // (mirrors the MethodInfo case) because that carrier does not exist in FSharp.Core. + | Fable.DeclaredType(entRef, _) when entRef.FullName = "System.Reflection.PropertyInfo" -> + let entName = + getEntityFullName + com + ctx + { entRef with FullName = "Microsoft.FSharp.Quotations.FSharpPropertyInfo" } + + makeFullNamePathTy entName None + // other declared types | Fable.DeclaredType(entRef, genArgs) -> transformEntityType com ctx entRef genArgs @@ -1559,6 +1632,17 @@ module Util = // unboxing value types or wrapped types | Fable.Any, t when isValueType com t || isWrappedType com t -> expr |> unboxValue com ctx t + // boxing a reference-typed record/union (Lrc-wrapped) to obj: coerce the + // smart pointer to `dyn Any` so its concrete pointee stays the struct. + | Fable.DeclaredType(entRef, _), Fable.Any when isReferenceRecordOrUnion com entRef -> + [ expr ] |> makeLibCall com ctx None "Native" "box_lrc" + + // unboxing obj back to a reference-typed record/union: downcast + re-wrap. + | Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef -> + let rawTy = transformEntityType com ctx entRef genArgs + let genArgsOpt = [ rawTy ] |> mkTypesGenericArgs + [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox_lrc" + // casts to generic param | _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value) @@ -1849,9 +1933,13 @@ module Util = // bindings we must emit a valid placeholder value instead (it is only there to // satisfy definite-assignment and gets overwritten before it is ever read). match typ with - | Fable.Any -> + | Fable.Any + | Fable.MetaType -> // `dyn Any` is unsized, so `Native::null` (needs a sized `NullableRef`) does // not apply; use a valid boxed-unit placeholder of type `LrcPtr`. + // `MetaType` (System.Type) is now also represented as `LrcPtr`, + // so it must take this path too (the `_` branch would emit a null of an + // unsized `dyn Any`, which does not compile). makeLibCall com ctx None "Native" "getZeroObj" [] | _ -> // Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref @@ -2060,10 +2148,84 @@ module Util = let fmt = makeFormatString parts makeFormatExpr com ctx fmt values + // Builds a rich reflection value for `typeof`. The emitted expression + // registers the record's field metadata + constructor/getter closures keyed by + // its concrete TypeId and returns a boxed RecordTypeInfo (the runtime value that + // a `System.Type` holds on the Rust target). This backs FSharpValue.MakeRecord, + // FSharpValue.GetRecordFields/GetRecordField and FSharpType.GetRecordFields. + let makeRecordTypeInfo (com: IRustCompiler) ctx r (entRef: Fable.EntityRef) genArgs (typ: Fable.Type) : Rust.Expr = + let ent = com.GetEntity(entRef) + let idents = getEntityFieldsAsIdents com ent + let objType = Fable.Any + let arrObjType = Fable.Array(objType, Fable.MutableArray) + + // tid = Reflection::type_id::() (concrete, unwrapped struct type) + let rawTy = transformEntityType com ctx entRef genArgs + let tidGenArgs = [ rawTy ] |> mkTypesGenericArgs + let tidExpr = makeLibCall com ctx tidGenArgs "Reflection" "type_id" [] + + // name + let nameExpr = makeStrConst ent.FullName |> transformExpr com ctx + + // field names: string[] + let fieldNamesExpr = + let names = idents |> List.map (fun ident -> makeStrConst ident.Name) + + Fable.Value(Fable.NewArray(Fable.ArrayValues names, Fable.String, Fable.MutableArray), None) + |> transformExpr com ctx + + // make: |vs: obj[]| box (Foo { X = unbox vs.[0]; Y = unbox vs.[1] }) + let makeExpr = + let vsIdent = makeTypedIdent arrObjType "vs" + + let ctorValues = + idents + |> List.mapi (fun i ident -> + let elem = + Fable.Get(Fable.IdentExpr vsIdent, Fable.ExprGet(makeIntConst i), objType, None) + + Fable.TypeCast(elem, ident.Type) // unbox to field type + ) + + let recordExpr = Fable.Value(Fable.NewRecord(ctorValues, entRef, genArgs), None) + let body = Fable.TypeCast(recordExpr, objType) // box the record + + Fable.Delegate([ vsIdent ], body, None, Fable.Tags.empty) + |> transformExpr com ctx + + // getters: (obj -> obj)[], each |o| box ((unbox o).Field) + let gettersExpr = + let getterFnType = Fable.DelegateType([ objType ], objType) + + let getters = + idents + |> List.map (fun ident -> + let oIdent = makeTypedIdent objType "o" + let recExpr = Fable.TypeCast(Fable.IdentExpr oIdent, typ) // unbox obj -> Foo + let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type) + let fieldGet = Fable.Get(recExpr, fieldInfo, ident.Type, None) + let body = Fable.TypeCast(fieldGet, objType) // box the field value + Fable.Delegate([ oIdent ], body, None, Fable.Tags.empty) + ) + + Fable.Value(Fable.NewArray(Fable.ArrayValues getters, getterFnType, Fable.MutableArray), None) + |> transformExpr com ctx + + makeLibCall com ctx None "Reflection" "recordType" [ tidExpr; nameExpr; fieldNamesExpr; makeExpr; gettersExpr ] + let makeTypeInfo (com: IRustCompiler) ctx r (typ: Fable.Type) : Rust.Expr = - let importName = getLibraryImportName com ctx "Reflection" "TypeId" - let genArgsOpt = transformGenArgs com ctx [ typ ] - makeFullNamePathExpr importName genArgsOpt + match typ with + | Fable.DeclaredType(entRef, genArgs) when (com.GetEntity(entRef)).IsFSharpRecord -> + makeRecordTypeInfo com ctx r entRef genArgs typ + | _ -> + // Non-record `System.Type` value: carry the concrete `TypeId`, boxed as + // `LrcPtr` so it flows through the same `obj` slot as record type + // infos. Emitting a bare `Reflection_::TypeId::` path is not a valid + // value expression, and comparing the boxed carrier's runtime `type_id()` + // (as `typeEquals` did) would make all non-record types compare equal. + let genArgsOpt = transformGenArgs com ctx [ typ ] + let tidExpr = makeLibCall com ctx genArgsOpt "Reflection" "type_id" [] + makeLibCall com ctx None "Native" "box_" [ tidExpr ] let transformValue (com: IRustCompiler) (ctx: Context) r value : Rust.Expr = let unimplemented () = @@ -3712,9 +3874,10 @@ module Util = mkUnitExpr () - | Fable.Quote _ -> - addError com [] None "Quotations are not yet supported for Rust target" - mkUnitExpr () + | Fable.Quote(body, _isTyped, _r) -> + // Lower the quoted body to runtime calls that build the quotation AST, + // then transform those like any other expression (mirrors JS/Python/Beam). + QuotationEmitter.emitQuotedExpr com body |> transformExpr com ctx let rec tryFindEntryPoint (com: IRustCompiler) decl : string list option = match decl with @@ -5280,7 +5443,19 @@ module Util = let genParams = FSharp2Fable.Util.getGenParamTypes genArgs - let ctx = { ctx with ScopedEntityGenArgs = getEntityGenParamNames ent } + let ctx = + // For object expressions the struct is parameterized by the actual + // instantiation gen args (from the enclosing scope), and member bodies + // reference those gen param names - not the interface's formal params. + // Using the interface's formal names here would fail to filter the type's + // gen params out of each member's own generics (emitting a spurious ``). + let scopedGenArgs = + if isObjectExpr then + genArgs |> FSharp2Fable.Util.getGenParamNames |> Set.ofList + else + getEntityGenParamNames ent + + { ctx with ScopedEntityGenArgs = scopedGenArgs } let isIgnoredMember (memb: Fable.MemberFunctionOrValue) = ent.IsFSharpExceptionDeclaration // to filter out compiler-generated exception equality @@ -5367,7 +5542,15 @@ module Util = |> List.collect (fun ifcEntRef -> let ifcGenArgs = if isObjectExpr then - genArgs + // The object expression's declared type gen args apply to its + // primary interface only. Inherited interfaces (e.g. IDisposable + // from IEnumerator<'T>) carry their own gen args, which may have a + // different arity - so blindly reusing genArgs would emit e.g. + // `IDisposable`. Look those up in the declared type's hierarchy. + if ifcEntRef.FullName = entRef.FullName then + genArgs + else + ifcEntRef |> findInterfaceGenArgs com ent else ifcEntRef |> findInterfaceGenArgs com ent diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 0af218b01a..87bdcf4cf8 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -490,8 +490,9 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Array _ -> Helper.LibCall(com, "Array", "equals", t, [ left; right ], ?loc = r) | List _ -> Helper.LibCall(com, "List", "equals", t, [ left; right ], ?loc = r) | IEnumerable -> Helper.LibCall(com, "Seq", "equals", t, [ left; right ], ?loc = r) - // | MetaType -> - // Helper.LibCall(com, "Reflection", "equals", t, [left; right], ?loc=r) + // System.Type is erased to a boxed reflection object (dyn Any, no PartialEq), + // so type-identity equality is compared on the carried TypeId at runtime. + | MetaType -> Helper.LibCall(com, "Reflection", "typeEquals", t, [ left; right ], ?loc = r) | HasReferenceEquality com _ -> referenceEquals com ctx r left right | Nullable _ -> // transforms null checks into option tests @@ -1298,10 +1299,13 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> objectHash com ctx r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - "Types can only be resolved at compile time. At runtime this will be same as `typeof`" - |> addWarning com ctx.InlinePath r - - makeTypeInfo r arg.Type |> Some + // Dynamic type of a boxed value: resolve via the runtime reflection + // registry (populated by typeof). Returns the concrete type's info + // when registered, else the value as an opaque placeholder. + Helper.LibCall(com, "Reflection", "getTypeFromObj", t, [ arg ], ?loc = r) + |> Some + else + makeTypeInfo r arg.Type |> Some | _ -> None let valueTypes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2875,14 +2879,66 @@ let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti let systemEnv (com: ICompiler) (ctx: Context) - (_: SourceLocation option) - (_: Type) + (r: SourceLocation option) + (t: Type) (i: CallInfo) (_: Expr option) - (_: Expr list) + (args: Expr list) = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") + | "GetEnvironmentVariable" -> + Helper.LibCall(com, "Environment", "getEnvironmentVariable", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "get_CurrentDirectory" -> + Helper.LibCall(com, "Environment", "getCurrentDirectory", t, [], ?loc = r) + |> Some + | _ -> None + +let paths (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = + match i.CompiledName with + | "Combine" -> + match args with + | [ _; _ ] -> + Helper.LibCall(com, "Path", "combine2", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | [ _; _; _ ] -> + Helper.LibCall(com, "Path", "combine3", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | _ -> None + | "GetDirectoryName" -> + Helper.LibCall(com, "Path", "getDirectoryName", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "GetFileName" -> + Helper.LibCall(com, "Path", "getFileName", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "GetFileNameWithoutExtension" -> + Helper.LibCall(com, "Path", "getFileNameWithoutExtension", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "GetExtension" -> + Helper.LibCall(com, "Path", "getExtension", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "HasExtension" -> + Helper.LibCall(com, "Path", "hasExtension", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "GetTempPath" -> Helper.LibCall(com, "Path", "getTempPath", t, [], ?loc = r) |> Some + | "GetRandomFileName" -> Helper.LibCall(com, "Path", "getRandomFileName", t, [], ?loc = r) |> Some + | _ -> None + +let files (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = + match i.CompiledName with + | "Exists" -> + Helper.LibCall(com, "File", "exists", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "WriteAllText" -> + Helper.LibCall(com, "File", "writeAllText", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "ReadAllText" -> + Helper.LibCall(com, "File", "readAllText", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "Delete" -> + Helper.LibCall(com, "File", "delete", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> None // Initial support, making at least InvariantCulture compile-able @@ -3258,19 +3314,37 @@ let uris = match i.CompiledName with | ".ctor" -> - Helper.LibCall(com, "Uri", "Uri.create", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + match args with + | [ ExprType String ] -> + Helper.LibCall(com, "Uri", "create", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | [ ExprType String; _ ] -> + Helper.LibCall(com, "Uri", "createWithKind", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | [ _; ExprType String ] -> + Helper.LibCall(com, "Uri", "createFromString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | [ _; _ ] -> + Helper.LibCall(com, "Uri", "createFromUri", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | _ -> None | "TryCreate" -> - Helper.LibCall(com, "Uri", "Uri.tryCreate", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + match args with + | (ExprType String) :: _ -> + Helper.LibCall(com, "Uri", "tryCreateWithKind", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | _ -> + Helper.LibCall(com, "Uri", "tryCreateFromUri", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "ToString" -> toString com ctx r [ thisArg.Value ] |> Some | "UnescapeDataString" -> - Helper.LibCall(com, "Util", "unescapeDataString", t, args, i.SignatureArgTypes) + Helper.LibCall(com, "Uri", "unescapeDataString", t, args, i.SignatureArgTypes) |> Some | "EscapeDataString" -> - Helper.LibCall(com, "Util", "escapeDataString", t, args, i.SignatureArgTypes) + Helper.LibCall(com, "Uri", "escapeDataString", t, args, i.SignatureArgTypes) |> Some | "EscapeUriString" -> - Helper.LibCall(com, "Util", "escapeUriString", t, args, i.SignatureArgTypes) + Helper.LibCall(com, "Uri", "escapeUriString", t, args, i.SignatureArgTypes) |> Some | "get_IsAbsoluteUri" | "get_Scheme" @@ -3280,11 +3354,7 @@ let uris | "get_PathAndQuery" | "get_Query" | "get_Fragment" - | "get_OriginalString" -> - Naming.removeGetSetPrefix i.CompiledName - |> Naming.lowerFirst - |> getFieldWith r t thisArg.Value - |> Some + | "get_OriginalString" -> makeMemberCall com ctx r t i "Uri" i.CompiledName thisArg args |> Some | _ -> None let laziness (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3617,6 +3687,8 @@ let private replacedModules = Types.timespan, timeSpans "System.Timers.Timer", timers "System.Environment", systemEnv + "System.IO.File", files + "System.IO.Path", paths "System.Globalization.CultureInfo", globalization "System.Random", random "System.Threading.CancellationToken", cancels @@ -3695,13 +3767,34 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr fsharpType com methName r t info args else fsharpValue com methName r t info args - | "Microsoft.FSharp.Reflection.UnionCaseInfo" + | "Microsoft.FSharp.Reflection.UnionCaseInfo" -> + // UnionCaseInfo from a NewUnionCase quotation deconstruction is a Rust-native + // FSharpUnionCaseInfo carrier; route member access to the quotation runtime. + match thisArg, info.CompiledName with + | Some c, "get_Name" -> Helper.LibCall(com, "quotation", "unionCaseName", t, [ c ], ?loc = r) |> Some + | Some c, "get_Tag" -> Helper.LibCall(com, "quotation", "unionCaseTag", t, [ c ], ?loc = r) |> Some + | _ -> None | "System.Reflection.PropertyInfo" | "System.Reflection.ParameterInfo" | "System.Reflection.MethodBase" | "System.Reflection.MethodInfo" | "System.Reflection.MemberInfo" -> + // Name/DeclaringType are inherited from MemberInfo, so `mi.Name` / `mi.DeclaringType` + // on a Call quotation binding arrive here even though the binding's type is MethodInfo. + // Detect that carrier via the thisArg's Fable type and route to the quotation runtime: + // get_Name -> a dedicated accessor (not the generic reflection name()); DeclaringType + // -> the declaring-type fullname boxed as System.Type, so mi.DeclaringType.FullName works. + let isMethodInfoCarrier (c: Expr) = + match c.Type with + | DeclaredType(e, _) -> e.FullName = "System.Reflection.MethodInfo" + | _ -> false + match thisArg, info.CompiledName with + | Some c, "get_Name" when isMethodInfoCarrier c -> + Helper.LibCall(com, "quotation", "methodName", t, [ c ], ?loc = r) |> Some + | Some c, "get_DeclaringType" when isMethodInfoCarrier c -> + Helper.LibCall(com, "quotation", "methodDeclaringType", t, [ c ], ?loc = r) + |> Some | Some c, "get_Tag" -> makeStrConst "tag" |> getExpr r t c |> Some | Some c, "get_ReturnType" -> makeStrConst "returnType" |> getExpr r t c |> Some | Some c, "GetParameters" -> makeStrConst "parameters" |> getExpr r t c |> Some @@ -3714,9 +3807,12 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr match c with | Value(TypeInfo(exprType, _), loc) -> getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some - | c -> Helper.LibCall(com, "Reflection", "name", t, [ c ], ?loc = r) |> Some + // Runtime PropertyInfo (e.g. a record field info): read its carried name. + // Distinct from the generic type-name helper `name()` to avoid a clash. + | c -> Helper.LibCall(com, "Reflection", "propertyName", t, [ c ], ?loc = r) |> Some | _ -> None - | _ -> None + // F# Quotations + | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with diff --git a/src/fable-library-rust/src/Async.rs b/src/fable-library-rust/src/Async.rs index b04f3eab4d..bd539baeaa 100644 --- a/src/fable-library-rust/src/Async.rs +++ b/src/fable-library-rust/src/Async.rs @@ -165,8 +165,11 @@ pub mod Monitor_ { LOCKS.get_or_init(|| RwLock::new(HashSet::new())) } - pub fn enter(o: LrcPtr) { - let p = Arc::::as_ptr(&o) as usize; + // `?Sized` so a boxed `obj` (LrcPtr) can be locked on: casting to + // `*const ()` first discards the vtable of a fat pointer, leaving the data + // address, which is what identifies the lock. + pub fn enter(o: LrcPtr) { + let p = Arc::::as_ptr(&o) as *const () as usize; loop { let otherHasLock = try_init_and_get_locks().read().unwrap().get(&p).is_some(); if otherHasLock { @@ -178,8 +181,8 @@ pub mod Monitor_ { } } - pub fn exit(o: LrcPtr) { - let p = Arc::::as_ptr(&o) as usize; + pub fn exit(o: LrcPtr) { + let p = Arc::::as_ptr(&o) as *const () as usize; let hasRemoved = try_init_and_get_locks().write().unwrap().remove(&p); if (!hasRemoved) { panic!("Not removed {}", p) diff --git a/src/fable-library-rust/src/Environment.rs b/src/fable-library-rust/src/Environment.rs new file mode 100644 index 0000000000..645e17d0e1 --- /dev/null +++ b/src/fable-library-rust/src/Environment.rs @@ -0,0 +1,21 @@ +#[cfg(not(feature = "no_std"))] +pub mod Environment_ { + use crate::Native_::NullableRef; + use crate::String_::{fromString, string}; + + // .NET's Environment.GetEnvironmentVariable returns null when the + // variable is not set, so we map a missing/invalid value to a null string. + pub fn getEnvironmentVariable(name: string) -> string { + match std::env::var(name.as_str()) { + Ok(value) => fromString(value), + Err(_) => ::null(), + } + } + + pub fn getCurrentDirectory() -> string { + match std::env::current_dir() { + Ok(path) => fromString(path.to_string_lossy().into_owned()), + Err(_) => string(""), + } + } +} diff --git a/src/fable-library-rust/src/Event.rs b/src/fable-library-rust/src/Event.rs new file mode 100644 index 0000000000..7f873bfce6 --- /dev/null +++ b/src/fable-library-rust/src/Event.rs @@ -0,0 +1,293 @@ +// F#-style events for Rust. Mirrors src/fable-library-ts/Event.ts and +// src/fable-library-py/fable_library/event.py. +// +// The Microsoft.FSharp.Control event *interfaces* (IDelegateEvent`1, IEvent`2) +// live in Interfaces.fs so they take part in Fable's namespace aggregation. +// The concrete runtime types (FSharpEvent`1, AnonymousEvent) live here in the +// `Event_` module; a transformType mapping in the Rust backend points the +// FSharpEvent`1 type at `Event_::FSharpEvent`1`. + +pub mod Event_ { + use crate::Choice_::Choice_2; + use crate::Microsoft::FSharp::Control::{IDelegateEvent_1, IEvent_2}; + use crate::Native_::{ + getZeroObj, referenceEquals, refCell, Any, Func0, Func1, Func2, LrcPtr, MutCell, Vec, + }; + use crate::Observable_::{mkDisposable, mkObserver, noCompleted, noError}; + use crate::System::{IDisposable, IObservable_1, IObserver_1}; + + // Handler<'T> = delegate of obj * 'T -> unit + pub type Handler = Func2, T, ()>; + + // ----- AnonymousEvent: the published IEvent ----- + // A concrete type so the Publish field can be used through IEvent_2 + // (AddHandler/RemoveHandler) *and* unsize-coerced to `dyn IObservable_1` + // (in F#/JS an IEvent is also an IObservable). + #[derive(Clone)] + pub struct AnonymousEvent { + add_handler: Func1, ()>, + remove_handler: Func1, ()>, + } + + impl core::fmt::Debug for AnonymousEvent { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for AnonymousEvent { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl AnonymousEvent { + fn do_sub(&self, observer: LrcPtr>) -> LrcPtr { + let h: Handler = + Func2::new(move |_sender: LrcPtr, args: T| observer.OnNext(args)); + (self.add_handler)(h.clone()); + let rem = self.remove_handler.clone(); + mkDisposable(Func0::new(move || rem(h.clone()))) + } + + // Inherent methods so calls on the concrete `Publish` field resolve + // without the interface trait having to be imported at the call site + // (Fable does not always emit a `use` for the flattened event trait). + pub fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + self.do_sub(observer) + } + pub fn AddHandler(&self, handler: Handler) { + (self.add_handler)(handler) + } + pub fn RemoveHandler(&self, handler: Handler) { + (self.remove_handler)(handler) + } + } + + impl IEvent_2, T> for AnonymousEvent { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + self.do_sub(observer) + } + fn AddHandler(&self, handler: Handler) { + (self.add_handler)(handler) + } + fn RemoveHandler(&self, handler: Handler) { + (self.remove_handler)(handler) + } + } + + impl IDelegateEvent_1> for AnonymousEvent { + fn AddHandler(&self, handler: Handler) { + (self.add_handler)(handler) + } + fn RemoveHandler(&self, handler: Handler) { + (self.remove_handler)(handler) + } + } + + impl IObservable_1 for AnonymousEvent { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + self.do_sub(observer) + } + } + + // ----- FSharpEvent`1 ----- + #[derive(Clone)] + pub struct FSharpEvent_1 { + handlers: LrcPtr>>>, + pub Publish: LrcPtr>, + } + + impl core::fmt::Debug for FSharpEvent_1 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for FSharpEvent_1 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl FSharpEvent_1 { + pub fn Trigger(&self, value: T) { + // Snapshot so a handler may unsubscribe during dispatch. + let hs = self.handlers.get().clone(); + for h in hs.iter() { + h(getZeroObj(), value.clone()); + } + } + } + + // Constructor for `new Event<'T>()` (routed here from the events replacement). + pub fn default() -> LrcPtr> { + let handlers: LrcPtr>>> = refCell(Vec::new()); + let add_handler = Func1::new({ + let handlers = handlers.clone(); + move |h: Handler| handlers.get_mut().push(h) + }); + let remove_handler = Func1::new({ + let handlers = handlers.clone(); + move |h: Handler| { + let vec = handlers.get_mut(); + // Compare by underlying function identity (the shared `dyn Fn` + // pointer). Func2's own PartialEq compares wrapper-handle + // addresses, which differ between clones, and the handler removed + // is a clone of the one stored. + if let Some(pos) = vec.iter().position(|x| referenceEquals(&**x, &*h)) { + vec.remove(pos); + } + } + }); + let publish = LrcPtr::new(AnonymousEvent { + add_handler, + remove_handler, + }); + LrcPtr::new(FSharpEvent_1 { + handlers, + Publish: publish, + }) + } + + // `CreateEvent`: build an IEvent from add/remove handler callbacks. + pub fn createEvent( + add_handler: Func1, ()>, + remove_handler: Func1, ()>, + ) -> LrcPtr> { + LrcPtr::new(AnonymousEvent { + add_handler, + remove_handler, + }) + } + + // ----- module functions ----- + + pub fn add( + callback: Func1, + source: LrcPtr, T>>, + ) { + source.Subscribe(mkObserver(callback, noError(), noCompleted())); + } + + pub fn choose( + chooser: Func1>, + source: LrcPtr, T>>, + ) -> LrcPtr, U>> { + let ev = default::(); + let evc = ev.clone(); + add( + Func1::new(move |t: T| { + if let Some(u) = chooser(t) { + evc.Trigger(u); + } + }), + source, + ); + ev.Publish.clone() + } + + pub fn filter( + predicate: Func1, + source: LrcPtr, T>>, + ) -> LrcPtr, T>> { + choose( + Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), + source, + ) + } + + pub fn map( + mapping: Func1, + source: LrcPtr, T>>, + ) -> LrcPtr, U>> { + let ev = default::(); + let evc = ev.clone(); + add(Func1::new(move |t: T| evc.Trigger(mapping(t))), source); + ev.Publish.clone() + } + + pub fn merge( + event1: LrcPtr, T>>, + event2: LrcPtr, T>>, + ) -> LrcPtr, T>> { + let ev = default::(); + let ev1 = ev.clone(); + let ev2 = ev.clone(); + add(Func1::new(move |t: T| ev1.Trigger(t)), event1); + add(Func1::new(move |t: T| ev2.Trigger(t)), event2); + ev.Publish.clone() + } + + pub fn pairwise( + source: LrcPtr, T>>, + ) -> LrcPtr>, LrcPtr<(T, T)>>> { + let ev = default::>(); + let evc = ev.clone(); + let last = refCell::>(None); + add( + Func1::new(move |next: T| { + if let Some(l) = last.get().clone() { + evc.Trigger(LrcPtr::new((l, next.clone()))); + } + last.set(Some(next)); + }), + source, + ); + ev.Publish.clone() + } + + pub fn partition( + predicate: Func1, + source: LrcPtr, T>>, + ) -> LrcPtr<( + LrcPtr, T>>, + LrcPtr, T>>, + )> { + let pred = predicate.clone(); + let first = filter(predicate, source.clone()); + let second = filter(Func1::new(move |x: T| !pred(x)), source); + LrcPtr::new((first, second)) + } + + pub fn scan( + collector: Func2, + state: U, + source: LrcPtr, T>>, + ) -> LrcPtr, U>> { + let st = refCell(state); + map( + Func1::new(move |t: T| { + let u = collector(st.get().clone(), t); + st.set(u.clone()); + u + }), + source, + ) + } + + pub fn split( + splitter: Func1>>, + source: LrcPtr, T>>, + ) -> LrcPtr<( + LrcPtr, U1>>, + LrcPtr, U2>>, + )> { + let sp = splitter.clone(); + let first = choose( + Func1::new(move |v: T| match &*sp(v) { + Choice_2::Choice1Of2(x) => Some(x.clone()), + _ => None, + }), + source.clone(), + ); + let second = choose( + Func1::new(move |v: T| match &*splitter(v) { + Choice_2::Choice2Of2(x) => Some(x.clone()), + _ => None, + }), + source, + ); + LrcPtr::new((first, second)) + } +} diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 0dffee4111..30ccc1242e 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -27,6 +27,8 @@ + + diff --git a/src/fable-library-rust/src/File.rs b/src/fable-library-rust/src/File.rs new file mode 100644 index 0000000000..acdcfc42bb --- /dev/null +++ b/src/fable-library-rust/src/File.rs @@ -0,0 +1,28 @@ +#[cfg(not(feature = "no_std"))] +pub mod File_ { + use crate::String_::{fromString, string}; + + pub fn exists(path: string) -> bool { + std::path::Path::new(path.as_str()).exists() + } + + pub fn writeAllText(path: string, contents: string) { + std::fs::write(path.as_str(), contents.as_str()) + .unwrap_or_else(|e| panic!("Could not write file '{}': {}", path.as_str(), e)) + } + + pub fn readAllText(path: string) -> string { + let contents = std::fs::read_to_string(path.as_str()) + .unwrap_or_else(|e| panic!("Could not read file '{}': {}", path.as_str(), e)); + fromString(contents) + } + + pub fn delete(path: string) { + // .NET File.Delete is a no-op when the file does not exist. + match std::fs::remove_file(path.as_str()) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => panic!("Could not delete file '{}': {}", path.as_str(), e), + } + } +} diff --git a/src/fable-library-rust/src/Interfaces.fs b/src/fable-library-rust/src/Interfaces.fs index 6974b5700a..54893b52fc 100644 --- a/src/fable-library-rust/src/Interfaces.fs +++ b/src/fable-library-rust/src/Interfaces.fs @@ -3,6 +3,14 @@ namespace System type IDisposable = abstract Dispose: unit -> unit +type IObserver<'T> = + abstract OnNext: 'T -> unit + abstract OnError: exn -> unit + abstract OnCompleted: unit -> unit + +type IObservable<'T> = + abstract Subscribe: IObserver<'T> -> IDisposable + type IEquatable<'T> = abstract Equals: 'T -> bool @@ -79,3 +87,19 @@ type IDictionary<'K, 'V> = abstract ContainsKey: 'K -> bool abstract TryGetValue: 'K * byref<'V> -> bool abstract Remove: 'K -> bool + + +namespace Microsoft.FSharp.Control + +type IDelegateEvent<'Delegate> = + abstract AddHandler: 'Delegate -> unit + abstract RemoveHandler: 'Delegate -> unit + +// NOTE: the second type parameter is named 'T (not 'Args) on purpose: when the +// Rust backend flattens the inherited System.IObservable<_>.Subscribe method into +// this trait it keeps IObservable's original parameter name ('T) verbatim instead +// of substituting the actual argument, so the names must line up or the generated +// trait references an undeclared `T`. +type IEvent<'Delegate, 'T> = + inherit IDelegateEvent<'Delegate> + inherit System.IObservable<'T> diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index 42a249daf3..a6c2383003 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -704,6 +704,48 @@ pub mod Native_ { try_downcast::<_, T>(&o).unwrap().clone() } + // Boxing/unboxing for reference-typed (Lrc-wrapped) declared types such as + // records and unions. Unlike `box_`, which wraps the value in a fresh Lrc, + // these coerce the *same* smart pointer to `dyn Any`, so the boxed value's + // concrete pointee is the record/union struct itself. That lets a plain + // downcast recover it, and lets `TypeId::of::()` (computed at the typeof + // site) match the pointee's runtime type id for value-based reflection. + #[cfg(not(feature = "lrc_ptr"))] + pub fn box_lrc(o: LrcPtr) -> LrcPtr { + o + } + + // With the `lrc_ptr` feature LrcPtr is a wrapper without CoerceUnsized, so + // coerce the inner Rc/Arc (which has it) and re-wrap. Same allocation, so + // pointer identity and the pointee's runtime type id are both preserved. + #[cfg(feature = "lrc_ptr")] + pub fn box_lrc(o: LrcPtr) -> LrcPtr { + let inner: Lrc = Lrc::clone(&*o); + let coerced: Lrc = inner; + LrcPtr::from(coerced) + } + + pub fn unbox_lrc(o: LrcPtr) -> LrcPtr { + #[cfg(not(feature = "lrc_ptr"))] + let inner: Lrc = o; + #[cfg(feature = "lrc_ptr")] + let inner: Lrc = Lrc::clone(&*o); + + // Downcast the smart pointer itself rather than cloning the value, so + // box_lrc + unbox_lrc round-trips pointer identity (ReferenceEquals). + // SAFETY: the `is::` check guarantees the allocation's pointee is a T; + // casting the fat pointer to *const T only drops the vtable. This is what + // Rc::downcast does internally, done manually because Arc only offers a + // stable downcast for Send + Sync pointees. + if (*inner).is::() { + let raw = Lrc::into_raw(inner) as *const T; + let typed = unsafe { Lrc::from_raw(raw) }; + fromFluent(typed) + } else { + panic!("Invalid unbox") + } + } + pub fn ofObj(value: T) -> Option { if is_null(&value) { None:: diff --git a/src/fable-library-rust/src/Observable.rs b/src/fable-library-rust/src/Observable.rs new file mode 100644 index 0000000000..826370874e --- /dev/null +++ b/src/fable-library-rust/src/Observable.rs @@ -0,0 +1,375 @@ +pub mod Observable_ { + use crate::Choice_::Choice_2; + use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr}; + use crate::System::{Exception, IDisposable, IObservable_1, IObserver_1}; + + // ----------------------------------------------------------- + // Observer + // ----------------------------------------------------------- + + #[derive(Clone)] + pub struct Observer { + on_next: Func1, + on_error: Func1, ()>, + on_completed: Func0<()>, + } + + impl core::fmt::Debug for Observer { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for Observer { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl IObserver_1 for Observer { + fn OnNext(&self, arg0: T) { + (self.on_next)(arg0) + } + fn OnError(&self, arg0: LrcPtr) { + (self.on_error)(arg0) + } + fn OnCompleted(&self) { + (self.on_completed)() + } + } + + // ----------------------------------------------------------- + // Observable + // ----------------------------------------------------------- + + #[derive(Clone)] + pub struct Observable { + subscribe: Func1>, LrcPtr>, + } + + impl core::fmt::Debug for Observable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for Observable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl IObservable_1 for Observable { + fn Subscribe(&self, arg0: LrcPtr>) -> LrcPtr { + (self.subscribe)(arg0) + } + } + + // ----------------------------------------------------------- + // Disposable + // ----------------------------------------------------------- + + #[derive(Clone)] + pub struct Disposable { + dispose: Func0<()>, + } + + impl core::fmt::Debug for Disposable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for Disposable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl IDisposable for Disposable { + fn Dispose(&self) { + (self.dispose)() + } + } + + // ----------------------------------------------------------- + // Constructors + // ----------------------------------------------------------- + + pub fn mkObserver( + on_next: Func1, + on_error: Func1, ()>, + on_completed: Func0<()>, + ) -> LrcPtr> { + interface_cast!( + LrcPtr::new(Observer { + on_next, + on_error, + on_completed, + }), + Lrc>, + ) + } + + fn mkObservable( + subscribe: Func1>, LrcPtr>, + ) -> LrcPtr> { + interface_cast!( + LrcPtr::new(Observable { subscribe }), + Lrc>, + ) + } + + pub fn mkDisposable(dispose: Func0<()>) -> LrcPtr { + interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc,) + } + + // A no-op error handler (errors are propagated by combinators when present, + // but the default subscriber ignores them, mirroring the JS/TS reference). + pub fn noError() -> Func1, ()> { + Func1::new(|_e: LrcPtr| ()) + } + + pub fn noCompleted() -> Func0<()> { + Func0::new(|| ()) + } + + // Build an error handler that forwards to a downstream observer. + fn fwdError( + observer: LrcPtr>, + ) -> Func1, ()> { + Func1::new(move |e: LrcPtr| observer.OnError(e)) + } + + // Build a completion handler that forwards to a downstream observer. + fn fwdCompleted(observer: LrcPtr>) -> Func0<()> { + Func0::new(move || observer.OnCompleted()) + } + + // ----------------------------------------------------------- + // Combinators + // ----------------------------------------------------------- + + pub fn add(callback: Func1, source: LrcPtr>) { + source.Subscribe(mkObserver(callback, noError(), noCompleted())); + } + + pub fn subscribe( + callback: Func1, + source: LrcPtr>, + ) -> LrcPtr { + source.Subscribe(mkObserver(callback, noError(), noCompleted())) + } + + pub fn choose( + chooser: Func1>, + source: LrcPtr>, + ) -> LrcPtr> { + mkObservable(Func1::new( + move |observer: LrcPtr>| -> LrcPtr { + let chooser = chooser.clone(); + let obs = observer.clone(); + let inner = mkObserver( + Func1::new(move |t: T| { + if let Some(u) = chooser(t) { + obs.OnNext(u); + } + }), + fwdError(observer.clone()), + fwdCompleted(observer.clone()), + ); + source.Subscribe(inner) + }, + )) + } + + pub fn filter( + predicate: Func1, + source: LrcPtr>, + ) -> LrcPtr> { + choose( + Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), + source, + ) + } + + pub fn map( + mapping: Func1, + source: LrcPtr>, + ) -> LrcPtr> { + mkObservable(Func1::new( + move |observer: LrcPtr>| -> LrcPtr { + let mapping = mapping.clone(); + let obs = observer.clone(); + let inner = mkObserver( + Func1::new(move |t: T| obs.OnNext(mapping(t))), + fwdError(observer.clone()), + fwdCompleted(observer.clone()), + ); + source.Subscribe(inner) + }, + )) + } + + pub fn merge( + source1: LrcPtr>, + source2: LrcPtr>, + ) -> LrcPtr> { + mkObservable(Func1::new( + move |observer: LrcPtr>| -> LrcPtr { + let stopped = refCell(false); + let completed1 = refCell(false); + let completed2 = refCell(false); + + let h1 = { + let stopped = stopped.clone(); + let completed1 = completed1.clone(); + let completed2 = completed2.clone(); + let obs_n = observer.clone(); + let obs_e = observer.clone(); + let obs_c = observer.clone(); + let se = stopped.clone(); + let sc = stopped.clone(); + source1.Subscribe(mkObserver( + Func1::new(move |v: T| { + if !*stopped.get() { + obs_n.OnNext(v); + } + }), + Func1::new(move |e: LrcPtr| { + if !*se.get() { + se.set(true); + obs_e.OnError(e); + } + }), + Func0::new(move || { + if !*sc.get() { + completed1.set(true); + if *completed2.get() { + sc.set(true); + obs_c.OnCompleted(); + } + } + }), + )) + }; + + let h2 = { + let stopped = stopped.clone(); + let completed1 = completed1.clone(); + let completed2 = completed2.clone(); + let obs_n = observer.clone(); + let obs_e = observer.clone(); + let obs_c = observer.clone(); + let se = stopped.clone(); + let sc = stopped.clone(); + source2.Subscribe(mkObserver( + Func1::new(move |v: T| { + if !*stopped.get() { + obs_n.OnNext(v); + } + }), + Func1::new(move |e: LrcPtr| { + if !*se.get() { + se.set(true); + obs_e.OnError(e); + } + }), + Func0::new(move || { + if !*sc.get() { + completed2.set(true); + if *completed1.get() { + sc.set(true); + obs_c.OnCompleted(); + } + } + }), + )) + }; + + mkDisposable(Func0::new(move || { + h1.Dispose(); + h2.Dispose(); + })) + }, + )) + } + + pub fn pairwise( + source: LrcPtr>, + ) -> LrcPtr>> { + mkObservable(Func1::new( + move |observer: LrcPtr>>| -> LrcPtr { + let last = refCell::>(None); + let obs = observer.clone(); + let inner = mkObserver( + Func1::new(move |next: T| { + if let Some(l) = last.get().clone() { + obs.OnNext(LrcPtr::new((l, next.clone()))); + } + last.set(Some(next)); + }), + fwdError(observer.clone()), + fwdCompleted(observer.clone()), + ); + source.Subscribe(inner) + }, + )) + } + + pub fn partition( + predicate: Func1, + source: LrcPtr>, + ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { + let pred = predicate.clone(); + let first = filter(predicate, source.clone()); + let second = filter(Func1::new(move |x: T| !pred(x)), source); + LrcPtr::new((first, second)) + } + + pub fn scan( + collector: Func2, + state: U, + source: LrcPtr>, + ) -> LrcPtr> { + mkObservable(Func1::new( + move |observer: LrcPtr>| -> LrcPtr { + let collector = collector.clone(); + let st = refCell(state.clone()); + let obs = observer.clone(); + let inner = mkObserver( + Func1::new(move |t: T| { + let u = collector(st.get().clone(), t); + st.set(u.clone()); + obs.OnNext(u); + }), + fwdError(observer.clone()), + fwdCompleted(observer.clone()), + ); + source.Subscribe(inner) + }, + )) + } + + pub fn split( + splitter: Func1>>, + source: LrcPtr>, + ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { + let sp = splitter.clone(); + let first = choose( + Func1::new(move |v: T| match &*sp(v) { + Choice_2::Choice1Of2(x) => Some(x.clone()), + _ => None, + }), + source.clone(), + ); + let second = choose( + Func1::new(move |v: T| match &*splitter(v) { + Choice_2::Choice2Of2(x) => Some(x.clone()), + _ => None, + }), + source, + ); + LrcPtr::new((first, second)) + } +} diff --git a/src/fable-library-rust/src/Path.rs b/src/fable-library-rust/src/Path.rs new file mode 100644 index 0000000000..d40f9d547e --- /dev/null +++ b/src/fable-library-rust/src/Path.rs @@ -0,0 +1,121 @@ +#[cfg(not(feature = "no_std"))] +pub mod Path_ { + use crate::String_::{fromString, string}; + + fn is_sep(c: char) -> bool { + c == '/' || c == '\\' + } + + // Index just after the last path separator, or 0 if none. + fn file_name_start(path: &str) -> usize { + match path.rfind(is_sep) { + Some(i) => i + 1, + None => 0, + } + } + + // .NET/JS-style: returns the substring after the last separator. + pub fn getFileName(path: string) -> string { + let s = path.as_str(); + fromString(s[file_name_start(s)..].to_string()) + } + + // Returns the directory portion (everything before the last separator), + // or an empty string when there is no separator. Trailing separators on + // the directory portion are trimmed, matching .NET semantics. + pub fn getDirectoryName(path: string) -> string { + let s = path.as_str(); + match s.rfind(is_sep) { + Some(i) => { + let dir = &s[..i]; + let dir = dir.trim_end_matches(is_sep); + fromString(dir.to_string()) + } + None => string(""), + } + } + + // Returns the extension including the leading dot, or an empty string. + // Matches Node's extname/.NET: a leading dot in the file name is not an + // extension (e.g. ".gitignore" has no extension). + pub fn getExtension(path: string) -> string { + let s = path.as_str(); + let name = &s[file_name_start(s)..]; + match name.rfind('.') { + Some(i) if i > 0 => fromString(name[i..].to_string()), + _ => string(""), + } + } + + pub fn getFileNameWithoutExtension(path: string) -> string { + let s = path.as_str(); + let name = &s[file_name_start(s)..]; + let stem = match name.rfind('.') { + Some(i) if i > 0 => &name[..i], + _ => name, + }; + fromString(stem.to_string()) + } + + pub fn hasExtension(path: string) -> bool { + let s = path.as_str(); + let name = &s[file_name_start(s)..]; + matches!(name.rfind('.'), Some(i) if i > 0) + } + + fn combine_str(a: &str, b: &str) -> String { + if a.is_empty() { + return b.to_string(); + } + if b.is_empty() { + return a.to_string(); + } + // .NET semantics: a rooted later argument discards the earlier one, + // e.g. Path.Combine("/foo", "/bar") == "/bar". + if b.starts_with(is_sep) { + return b.to_string(); + } + let a = a.trim_end_matches(is_sep); + format!("{}/{}", a, b) + } + + pub fn combine2(path1: string, path2: string) -> string { + fromString(combine_str(path1.as_str(), path2.as_str())) + } + + pub fn combine3(path1: string, path2: string, path3: string) -> string { + let ab = combine_str(path1.as_str(), path2.as_str()); + fromString(combine_str(&ab, path3.as_str())) + } + + pub fn getTempPath() -> string { + fromString(std::env::temp_dir().to_string_lossy().into_owned()) + } + + // Non-cryptographic randomness derived from the system clock and an + // atomic counter; good enough for unique temp file names. + fn rand_u64() -> u64 { + use core::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + let c = COUNTER.fetch_add(1, Ordering::Relaxed); + let mut x = nanos ^ c.wrapping_mul(0x9E37_79B9_7F4A_7C15); + x ^= x >> 30; + x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9); + x ^= x >> 27; + x = x.wrapping_mul(0x94D0_49BB_1331_11EB); + x ^= x >> 31; + x + } + + // Mirrors .NET's Path.GetRandomFileName(): 8 random hex chars, a dot, + // then 3 more random hex chars. + pub fn getRandomFileName() -> string { + let name = (rand_u64() as u32) & 0xFFFF_FFFF; + let ext = (rand_u64() as u32) & 0xFFF; + fromString(format!("{:08x}.{:03x}", name, ext)) + } +} diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs new file mode 100644 index 0000000000..f10f2e517c --- /dev/null +++ b/src/fable-library-rust/src/Quotation.fs @@ -0,0 +1,319 @@ +// F# quotation runtime for the Fable Rust target (functions). +// +// Authored in F# and transpiled by Fable so the FSharpExpr/FSharpVar types land at the +// right namespace and every mk*/is* signature matches what generated user code expects. +// The module is named `quotation_` to match the `libModule = "quotation"` import path. +module quotation_ + +open Microsoft.FSharp.Quotations + +// --- Var constructor + accessors --- + +let mkQuotVar (name: string) (typ: string) (isMutable: bool) : FSharpVar = + { + Name = name + Type = typ + IsMutable = isMutable + } + +let varGetName (v: FSharpVar) : string = v.Name +let varGetType (v: FSharpVar) : string = v.Type +let varGetIsMutable (v: FSharpVar) : bool = v.IsMutable + +// --- Expr constructors --- + +// Generic so callers can pass an unboxed literal (i32/bool/string/...); `box` erases +// it to obj. A non-generic obj parameter would require the caller to box, which the +// emitter does not do on statically typed targets. +let mkValue<'T> (value: 'T) (typ: string) : FSharpExpr = ExprValue(box value, typ) + +// A null-value node (no instance / null literal / empty option or list). +// The payload is a boxed sentinel (not a null literal, which the Rust target can't +// represent for an untyped obj); consumers distinguish null nodes by the type tag. +let mkNull (typ: string) : FSharpExpr = ExprValue(box 0, typ) + +let mkVar (v: FSharpVar) : FSharpExpr = ExprVarExpr v +let mkLambda (v: FSharpVar) (body: FSharpExpr) : FSharpExpr = ExprLambda(v, body) +let mkApplication (func: FSharpExpr) (arg: FSharpExpr) : FSharpExpr = ExprApplication(func, arg) +let mkLet (v: FSharpVar) (value: FSharpExpr) (body: FSharpExpr) : FSharpExpr = ExprLet(v, value, body) + +let mkIfThenElse (guard: FSharpExpr) (thenExpr: FSharpExpr) (elseExpr: FSharpExpr) : FSharpExpr = + ExprIfThenElse(guard, thenExpr, elseExpr) + +let mkCall (instance: FSharpExpr) (method: string) (args: FSharpExpr[]) (declaringType: string) : FSharpExpr = + ExprCall(instance, method, args, declaringType) + +let mkSequential (first: FSharpExpr) (second: FSharpExpr) : FSharpExpr = ExprSequential(first, second) + +// A correctly-typed empty argument array. Rust is statically typed, so an empty +// obj[] built by the emitter won't unify with the FSharpExpr[] parameters of +// mkCall/mkNewUnion/mkNewTuple (zero-arg calls like property getters, nullary +// union cases). The emitter routes empty expr-arrays here for the Rust target. +let emptyExprArray () : FSharpExpr[] = [||] + +let mkNewTuple (elements: FSharpExpr[]) : FSharpExpr = ExprNewTuple elements + +let mkNewUnion (typeName: string) (tag: int) (caseName: string) (fields: FSharpExpr[]) : FSharpExpr = + ExprNewUnion(typeName, tag, caseName, fields) + +let mkNewRecord (typeName: string) (fieldNames: string[]) (values: FSharpExpr[]) : FSharpExpr = + ExprNewRecord(typeName, fieldNames, values) + +// --- UnionCaseInfo accessors (backing uci.Name / uci.Tag on a NewUnionCase binding) --- + +let unionCaseName (u: FSharpUnionCaseInfo) : string = u.Name +let unionCaseTag (u: FSharpUnionCaseInfo) : int = u.Tag + +// --- MethodInfo accessors (backing mi.Name / mi.DeclaringType on a Call binding) --- + +// mi.Name -> string (dedicated accessor; distinct from the generic reflection name()). +let methodName (m: FSharpMethodInfo) : string = m.Name + +// mi.DeclaringType -> System.Type. Rust has no faithful System.Type runtime, so the +// declaring type is represented by its fullname string boxed as obj (System.Type erases +// to Any on Rust). mi.DeclaringType.FullName then routes to Reflection.fullName, which +// unboxes the string. +let methodDeclaringType (m: FSharpMethodInfo) : obj = box m.DeclaringType +let mkNewList (head: FSharpExpr) (tail: FSharpExpr) : FSharpExpr = ExprNewList(head, tail) +let mkTupleGet (e: FSharpExpr) (index: int) : FSharpExpr = ExprTupleGet(e, index) +let mkUnionTag (e: FSharpExpr) : FSharpExpr = ExprUnionTag e +let mkUnionField (e: FSharpExpr) (fieldIndex: int) : FSharpExpr = ExprUnionField(e, fieldIndex) +let mkFieldGet (e: FSharpExpr) (fieldName: string) : FSharpExpr = ExprFieldGet(e, fieldName) +let mkFieldSet (e: FSharpExpr) (fieldName: string) (value: FSharpExpr) : FSharpExpr = ExprFieldSet(e, fieldName, value) +let mkVarSet (target: FSharpExpr) (value: FSharpExpr) : FSharpExpr = ExprVarSet(target, value) + +// --- Type accessor --- + +let getType (e: FSharpExpr) : string = + match e with + | ExprValue(_, t) -> t + | ExprLambda(v, _) -> v.Type + | _ -> "obj" + +// --- Pattern helpers (return option, following the active-pattern convention) --- + +let isValue (e: FSharpExpr) = + match e with + | ExprValue(v, t) -> Some(v, t) + | _ -> None + +let isVar (e: FSharpExpr) = + match e with + | ExprVarExpr v -> Some v + | _ -> None + +let isLambda (e: FSharpExpr) = + match e with + | ExprLambda(v, b) -> Some(v, b) + | _ -> None + +let isApplication (e: FSharpExpr) = + match e with + | ExprApplication(f, a) -> Some(f, a) + | _ -> None + +let isLet (e: FSharpExpr) = + match e with + | ExprLet(v, value, body) -> Some(v, value, body) + | _ -> None + +let isIfThenElse (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) -> Some(g, t, el) + | _ -> None + +let isCall (e: FSharpExpr) = + match e with + | ExprCall(instance, m, args, dt) -> + // A static/operator call carries the "novalue" node as its instance; + // expose it as None so Patterns.Call matches F#. The tag is distinct from + // "null", which is a genuine quoted null value. + let inst = + match instance with + | ExprValue(_, "novalue") -> None + | _ -> Some instance + + // Build the MethodInfo carrier from the stored compiled-name + declaring-type + // fullname, so the Call binding exposes the real F# shape (mi.Name / + // mi.DeclaringType.FullName). This is the SQLProvider linchpin: the declaring + // type distinguishes List.map from Array.map. + let mi: FSharpMethodInfo = + { + Name = m + DeclaringType = dt + } + + Some(inst, mi, List.ofArray args) + | _ -> None + +let isSequential (e: FSharpExpr) = + match e with + | ExprSequential(f, s) -> Some(f, s) + | _ -> None + +let isNewTuple (e: FSharpExpr) = + match e with + | ExprNewTuple els -> Some(List.ofArray els) + | _ -> None + +let isNewUnionCase (e: FSharpExpr) = + match e with + | ExprNewUnion(typeName, tag, caseName, fields) -> + // Return F#'s (UnionCaseInfo * Expr list) shape. The UnionCaseInfo carrier + // backs uci.Name/uci.Tag via unionCaseName/unionCaseTag. + let uci: FSharpUnionCaseInfo = + { + Name = caseName + Tag = tag + DeclaringType = typeName + } + + Some(uci, List.ofArray fields) + | _ -> None + +let isNewRecord (e: FSharpExpr) = + match e with + | ExprNewRecord(typeName, _names, values) -> + // Return F#'s (Type * Expr list) shape. Rust has no faithful System.Type runtime, + // so the type slot is erased to a boxed value (the record type name). The Rust + // compiler erases the pattern's System.Type slot to Any to match (see Replacements). + Some(box typeName, List.ofArray values) + | _ -> None + +let isTupleGet (e: FSharpExpr) = + match e with + | ExprTupleGet(inner, i) -> Some(inner, i) + | _ -> None + +let isFieldGet (e: FSharpExpr) = + match e with + | ExprFieldGet(inner, n) -> + // Return F#'s (Expr option * PropertyInfo * Expr list) shape, as JS/TS/Python do. + // A static property get carries the "novalue" node as its target (distinct from + // "null", a genuine quoted null); expose it as None so Patterns.PropertyGet matches + // F#. The third slot mirrors PropertyGet's indexer args, always empty here. + let inst = + match inner with + | ExprValue(_, "novalue") -> None + | _ -> Some inner + + let pi: FSharpPropertyInfo = { Name = n } + Some(inst, pi, ([]: FSharpExpr list)) + | _ -> None + +// --- Free variables --- + +let getFreeVars (e: FSharpExpr) : FSharpVar list = + let seen = System.Collections.Generic.HashSet() + let acc = ResizeArray() + + let rec walk (bound: Set) (e: FSharpExpr) = + match e with + | ExprVarExpr v -> + if not (bound.Contains v.Name) && seen.Add v.Name then + acc.Add v + | ExprLambda(v, body) -> walk (bound.Add v.Name) body + | ExprLet(v, value, body) -> + walk bound value + walk (bound.Add v.Name) body + | ExprApplication(f, a) -> + walk bound f + walk bound a + | ExprIfThenElse(g, t, el) -> + walk bound g + walk bound t + walk bound el + | ExprCall(_, _, args, _) -> + for a in args do + walk bound a + | ExprSequential(f, s) -> + walk bound f + walk bound s + | ExprNewTuple els -> + for el in els do + walk bound el + | ExprTupleGet(inner, _) -> walk bound inner + | ExprFieldGet(inner, _) -> walk bound inner + | _ -> () + + walk Set.empty e + List.ofSeq acc + +// --- Evaluation (structural cases + common operators; SQL translation deconstructs +// rather than evaluates, so this covers the tested subset). --- + +let private applyOperator (method: string) (args: obj list) : obj = + // Extract args positionally rather than binding them in the match: pattern-binding + // obj (Rc) variables makes Fable zero-initialize a fat pointer, which is + // invalid at runtime. Matching only on the method string avoids that. + // if/elif (not match): Fable-Rust would emit string literals as match patterns, + // which isn't valid Rust; an if-chain compiles to string == comparisons. + let i (n: int) : int = unbox (List.item n args) + let b (n: int) : bool = unbox (List.item n args) + + if method = "op_Addition" then + box (i 0 + i 1) + elif method = "op_Subtraction" then + box (i 0 - i 1) + elif method = "op_Multiply" then + box (i 0 * i 1) + elif method = "op_Division" then + box (i 0 / i 1) + elif method = "op_Modulus" then + box (i 0 % i 1) + elif method = "op_UnaryNegation" then + box (-(i 0)) + elif method = "op_Equality" then + box (i 0 = i 1) + elif method = "op_Inequality" then + box (i 0 <> i 1) + elif method = "op_LessThan" then + box (i 0 < i 1) + elif method = "op_LessThanOrEqual" then + box (i 0 <= i 1) + elif method = "op_GreaterThan" then + box (i 0 > i 1) + elif method = "op_GreaterThanOrEqual" then + box (i 0 >= i 1) + elif method = "op_BooleanAnd" then + box (b 0 && b 1) + elif method = "op_BooleanOr" then + box (b 0 || b 1) + elif method = "op_LogicalNot" then + box (not (b 0)) + else + failwithf "Cannot evaluate method: %s" method + +let evaluate (e: FSharpExpr) : obj = + let rec eval (env: Map) (e: FSharpExpr) : obj = + match e with + | ExprValue(v, _) -> v + | ExprVarExpr v -> Map.find v.Name env + | ExprLambda(v, body) -> box (fun (arg: obj) -> eval (Map.add v.Name arg env) body) + | ExprApplication(f, a) -> + let func = unbox obj> (eval env f) + func (eval env a) + | ExprLet(v, value, body) -> eval (Map.add v.Name (eval env value) env) body + | ExprIfThenElse(g, t, el) -> + if unbox (eval env g) then + eval env t + else + eval env el + | ExprSequential(a, b) -> + eval env a |> ignore + eval env b + | ExprCall(_, method, args, _) -> applyOperator method [ for a in args -> eval env a ] + // A tuple literal evaluates to a boxed obj[] of its evaluated elements, mirroring + // PHP (which produces a plain array). TupleGet then indexes into that array; without + // this arm `eval inner` on a tuple literal would fall through to failwith and there + // would be nothing indexable, so both arms are needed together. + | ExprNewTuple els -> box [| for el in els -> eval env el |] + | ExprTupleGet(inner, index) -> + // The match-bound `index` is a borrowed &i32 on Rust while the array Index + // operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the + // arithmetic dereferences the borrow) so the indexing type-checks. + let i = index + 0 + (unbox (eval env inner)).[i] + | _ -> failwith "Cannot evaluate expression" + + eval Map.empty e diff --git a/src/fable-library-rust/src/QuotationTypes.fs b/src/fable-library-rust/src/QuotationTypes.fs new file mode 100644 index 0000000000..d853a33b26 --- /dev/null +++ b/src/fable-library-rust/src/QuotationTypes.fs @@ -0,0 +1,65 @@ +// F# quotation AST types for the Fable Rust target. +// +// Declared in the Microsoft.FSharp.Quotations namespace with the *compiled* names +// (FSharpExpr / FSharpVar) that Fable maps the real FSharp.Core Quotations.Expr / .Var +// to on the Rust target. Using the compiled names (rather than Expr/Var) means no clash +// with FSharp.Core during F# type-checking, while still resolving to the same Rust path +// (Microsoft::FSharp::Quotations::FSharpExpr) that generated user code references. +namespace Microsoft.FSharp.Quotations + +type FSharpVar = + { + Name: string + Type: string + IsMutable: bool + } + +// Rust-native carrier for Microsoft.FSharp.Reflection.UnionCaseInfo. Fable-Rust +// erases the F# UnionCaseInfo type to this struct (see Fable2Rust.transformType), +// so `uci.Name`/`uci.Tag` on a NewUnionCase binding route to the accessors below. +type FSharpUnionCaseInfo = + { + Name: string + Tag: int + DeclaringType: string + } + +// Rust-native carrier for System.Reflection.MethodInfo. Fable-Rust erases the F# +// MethodInfo type to this struct (see Fable2Rust.transformType), so `mi.Name` and +// `mi.DeclaringType` on a Call quotation deconstruction route to the accessors in +// Quotation.fs. DeclaringType is stored as the declaring-type fullname string (the +// emitter fills it from declaringEntity.FullName, e.g. "...Collections.ListModule"). +type FSharpMethodInfo = + { + Name: string + DeclaringType: string + } + +// Rust-native carrier for System.Reflection.PropertyInfo. Fable-Rust erases the F# +// PropertyInfo type to this struct (see Fable2Rust.transformType), so it is the single +// carrier for both quotation deconstruction (PropertyGet's propInfo) and reflection +// (FSharpType.GetRecordFields), matching .NET where both yield the same PropertyInfo. +// It carries only the name, as a quotation knows nothing else about the property: the +// field getter is resolved from the reflection registry by name when a value is read, +// mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. +type FSharpPropertyInfo = { Name: string } + +type FSharpExpr = + | ExprValue of value: obj * typ: string + | ExprVarExpr of var: FSharpVar + | ExprLambda of var: FSharpVar * body: FSharpExpr + | ExprApplication of func: FSharpExpr * arg: FSharpExpr + | ExprLet of var: FSharpVar * value: FSharpExpr * body: FSharpExpr + | ExprIfThenElse of guard: FSharpExpr * thenExpr: FSharpExpr * elseExpr: FSharpExpr + | ExprCall of instance: FSharpExpr * method: string * args: FSharpExpr[] * declaringType: string + | ExprSequential of first: FSharpExpr * second: FSharpExpr + | ExprNewTuple of elements: FSharpExpr[] + | ExprNewUnion of typeName: string * tag: int * caseName: string * fields: FSharpExpr[] + | ExprNewRecord of typeName: string * fieldNames: string[] * values: FSharpExpr[] + | ExprNewList of head: FSharpExpr * tail: FSharpExpr + | ExprTupleGet of expr: FSharpExpr * index: int + | ExprUnionTag of expr: FSharpExpr + | ExprUnionField of expr: FSharpExpr * fieldIndex: int + | ExprFieldGet of expr: FSharpExpr * fieldName: string + | ExprFieldSet of expr: FSharpExpr * fieldName: string * value: FSharpExpr + | ExprVarSet of target: FSharpExpr * value: FSharpExpr diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 6a26cf05ca..713b62ecc2 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -1,11 +1,237 @@ pub mod Reflection_ { pub use core::any::TypeId; // re-export - //use crate::Native_::{LrcPtr}; + use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; + use crate::Native_::{box_, Any, Func1, LrcPtr, Vec}; + use crate::NativeArray_::{array_from, Array}; use crate::String_::string; + #[cfg(not(feature = "no_std"))] + use core::cell::RefCell; + #[cfg(not(feature = "no_std"))] + use std::collections::HashMap; + pub fn name() -> string { // TODO: map some common type names to .NET type names string(core::any::type_name::()) } + + // The object (System.Object / obj) representation on the Rust target. + type obj = LrcPtr; + + // Compile-time helper emitted by `typeof` to obtain a concrete type id. + pub fn type_id() -> TypeId { + TypeId::of::() + } + + // PropertyInfo-like carrier for a single record field. + // `get` downcasts the boxed record and reads the field, returning it boxed. + #[derive(Clone)] + pub struct RecordFieldInfo { + pub name: string, + pub get: Func1, + } + + // Rich reflection info attached to `typeof`. Carries everything + // needed for MakeRecord (the `make` constructor closure) as well as + // GetRecordFields/GetRecordField (field names + per-field getters). + // This mirrors the JS/TS model where the reflected Type is an object + // carrying field metadata, so the same F# reflection code behaves the same. + #[derive(Clone)] + pub struct RecordTypeInfo { + pub tid: TypeId, + pub name: string, + pub fields: Array>, + pub make: Func1, obj>, + } + + // Registry mapping a record's *concrete* TypeId to its reflection info. + // Populated when `typeof` is evaluated. This is what makes + // value-based reflection (GetRecordFields(record)) possible: from a bare + // boxed record we can recover its runtime TypeId and look the info up. + // + // `thread_local!` is std-only, so under `no_std` there is no registry and + // the value-first entry points degrade (like exception catching does). + // Type-first reflection (typeof, MakeRecord, GetRecordElements) is + // unaffected, as those carry the info in the `System.Type` value itself. + #[cfg(not(feature = "no_std"))] + thread_local! { + static RECORD_REGISTRY: RefCell> = + RefCell::new(HashMap::new()); + } + + #[cfg(not(feature = "no_std"))] + fn registry_insert(tid: TypeId, info: RecordTypeInfo) { + RECORD_REGISTRY.with(|r| { + r.borrow_mut().insert(tid, info); + }); + } + + #[cfg(feature = "no_std")] + fn registry_insert(_tid: TypeId, _info: RecordTypeInfo) { + // no registry when no_std + } + + #[cfg(not(feature = "no_std"))] + fn registry_get(tid: &TypeId) -> Option { + RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) + } + + #[cfg(feature = "no_std")] + fn registry_get(_tid: &TypeId) -> Option { + None // no registry when no_std + } + + // Emitted by generated `typeof`. Builds the field metadata, registers + // the type by its concrete TypeId, and returns the boxed RecordTypeInfo + // (which is what a `System.Type` value holds on the Rust target). + pub fn recordType( + tid: TypeId, + name: string, + field_names: Array, + make: Func1, obj>, + getters: Array>, + ) -> obj { + let names: Vec = field_names.get().iter().cloned().collect(); + let gets: Vec> = getters.get().iter().cloned().collect(); + let fields_vec: Vec> = names + .into_iter() + .zip(gets.into_iter()) + .map(|(n, g)| LrcPtr::new(RecordFieldInfo { name: n, get: g })) + .collect(); + let info = RecordTypeInfo { + tid, + name, + fields: array_from(fields_vec), + make, + }; + registry_insert(tid, info.clone()); + box_(info) + } + + fn type_info_of(typ: &obj) -> RecordTypeInfo { + (**typ) + .downcast_ref::() + .expect("Type does not carry record reflection info") + .clone() + } + + // FSharpValue.MakeRecord(typ, values, ?bindingFlags) -> obj + pub fn makeRecord(typ: obj, values: Array, _flags: Option) -> obj { + let info = type_info_of(&typ); + (info.make)(values) + } + + // Resolves a field getter for a record value by property name, via the registry. + // The PropertyInfo carrier holds only a name (a quotation knows nothing more), so the + // getter is looked up here, mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. + fn getterOf(record: &obj, name: &string) -> Func1 { + let tid = (**record).type_id(); + let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); + let field = info + .fields + .get() + .iter() + .find(|f| &f.name == name) + .cloned() + .expect("Property not found on record type"); + field.get.clone() + } + + // FSharpType.GetRecordFields(typ, ?bindingFlags) -> PropertyInfo[] + pub fn getRecordElements(typ: obj, _flags: Option) -> Array> { + let info = type_info_of(&typ); + let props: Vec> = info + .fields + .get() + .iter() + .map(|f| LrcPtr::new(FSharpPropertyInfo { Name: f.name.clone() })) + .collect(); + array_from(props) + } + + // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] + pub fn getRecordFields(record: obj, _flags: Option) -> Array { + let tid = (*record).type_id(); + let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); + let vals: Vec = info + .fields + .get() + .iter() + .map(|f| (f.get)(record.clone())) + .collect(); + array_from(vals) + } + + // FSharpValue.GetRecordField(record, propInfo) -> obj + pub fn getRecordField(record: obj, info: LrcPtr) -> obj { + let get = getterOf(&record, &info.Name); + get(record) + } + + // PropertyInfo.Name -> string (dedicated accessor; distinct from name()). + pub fn propertyName(info: LrcPtr) -> string { + info.Name.clone() + } + + // PropertyInfo.GetValue(record) -> obj + pub fn getValue(info: LrcPtr, record: obj) -> obj { + let get = getterOf(&record, &info.Name); + get(record) + } + + // FSharpType.IsRecord(typ) -> bool + pub fn isRecord(typ: obj, _flags: Option) -> bool { + (*typ).downcast_ref::().is_some() + } + + // System.Type.FullName. On Rust a `System.Type` value is a boxed carrier: a record + // type carries RecordTypeInfo (use its registered name), while a quotation-derived + // declaring type (from MethodInfo.DeclaringType) carries the boxed fullname string. + pub fn fullName(typ: obj) -> string { + if let Some(info) = (*typ).downcast_ref::() { + info.name.clone() + } else if let Some(s) = (*typ).downcast_ref::() { + s.clone() + } else { + crate::String_::string("") + } + } + + // obj.GetType() for a statically-erased value (static type = obj/Any). + // Reads the boxed value's *runtime* type id and returns the concrete type's + // registered reflection info, so `(box record : obj).GetType()` resolves to + // the real record type (not typeof) whenever that type was registered + // via a prior `typeof`. Falls back to returning the object itself as an + // opaque placeholder for unregistered types. + pub fn getTypeFromObj(o: obj) -> obj { + let tid = (*o).type_id(); + match registry_get(&tid) { + Some(info) => box_(info), + None => o, + } + } + + // System.Type equality (typeof = typeof). Compares the carried concrete + // TypeId when both operands are record type infos; otherwise falls back to + // comparing the boxed values' runtime type ids. + pub fn typeEquals(a: obj, b: obj) -> bool { + let ta = (*a).downcast_ref::(); + let tb = (*b).downcast_ref::(); + match (ta, tb) { + (Some(x), Some(y)) => x.tid == y.tid, + (None, None) => { + // Non-record `System.Type` values carry a boxed concrete `TypeId`. + // Downcast both and compare the *carried* ids; comparing the boxed + // carriers' runtime `type_id()` would always be `TypeId::of::()` + // and therefore make every non-record type compare equal. + match ((*a).downcast_ref::(), (*b).downcast_ref::()) { + (Some(x), Some(y)) => x == y, + _ => (*a).type_id() == (*b).type_id(), + } + } + // One record, one non-record: definitely different types. + _ => false, + } + } } diff --git a/src/fable-library-rust/src/Uri.rs b/src/fable-library-rust/src/Uri.rs new file mode 100644 index 0000000000..dddf187d05 --- /dev/null +++ b/src/fable-library-rust/src/Uri.rs @@ -0,0 +1,507 @@ +#[cfg_attr(rustfmt, rustfmt::skip)] +pub mod Uri_ { + use crate::Native_::{format, LrcPtr, MutCell, String, ToString, Vec}; + use crate::String_::{fromSlice, fromString, string}; + + // UriKind: 0 = RelativeOrAbsolute, 1 = Absolute, 2 = Relative + + #[derive(Clone, Default, PartialEq)] + pub struct Uri { + isAbsolute: bool, + hasAuthority: bool, // true for scheme://authority/... , false for scheme:path (mailto:, tel:, urn:) + original: string, + relative: string, // raw string for relative URIs + schemePart: string, + hostPart: string, + port: i32, // -1 when no explicit port + pathPart: string, + queryPart: string, // "" or "?..." + fragmentPart: string, // "" or "#..." + } + + // ------------------------------------------------------------------- + // helpers + // ------------------------------------------------------------------- + + // Returns the byte index of the ':' that terminates a valid URI scheme + // prefix (scheme = ALPHA *(ALPHA / DIGIT / "+" / "-" / ".")), or None when + // `s` has no scheme (i.e. it is a relative reference). This recognizes both + // `scheme://authority/...` and authority-less forms like `mailto:a@b.com`. + fn scheme_end(s: &str) -> Option { + let bytes = s.as_bytes(); + match bytes.first() { + Some(&c) if (c as char).is_ascii_alphabetic() => {} + _ => return None, + } + let mut i = 1; + while i < bytes.len() { + let c = bytes[i] as char; + if c == ':' { + return Some(i); + } else if c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.' { + i += 1; + } else { + return None; + } + } + None + } + + fn is_absolute_str(s: &str) -> bool { + scheme_end(s).is_some() + } + + // RFC 3986 §5.2.4 "remove dot segments": collapses `.` and `..` in a path. + fn remove_dot_segments(path: &str) -> String { + let mut input = path.to_string(); + let mut output = String::new(); + while !input.is_empty() { + if input.starts_with("../") { + input.replace_range(0..3, ""); + } else if input.starts_with("./") { + input.replace_range(0..2, ""); + } else if input.starts_with("/./") { + input.replace_range(0..3, "/"); + } else if input == "/." { + input = "/".to_string(); + } else if input.starts_with("/../") { + input.replace_range(0..4, "/"); + remove_last_segment(&mut output); + } else if input == "/.." { + input = "/".to_string(); + remove_last_segment(&mut output); + } else if input == "." || input == ".." { + input.clear(); + } else { + let start = if input.starts_with('/') { 1 } else { 0 }; + let end = match input[start..].find('/') { + Some(i) => start + i, + None => input.len(), + }; + output.push_str(&input[..end]); + input.replace_range(0..end, ""); + } + } + output + } + + fn remove_last_segment(output: &mut String) { + match output.rfind('/') { + Some(i) => output.truncate(i), + None => output.clear(), + } + } + + fn is_default_port(scheme: &str, port: i32) -> bool { + port == -1 + || (scheme == "http" && port == 80) + || (scheme == "https" && port == 443) + } + + fn authority_str(scheme: &str, host: &str, port: i32) -> String { + if is_default_port(scheme, port) { + host.to_string() + } else { + format!("{}:{}", host, port) + } + } + + fn hex_val(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } + } + + fn percent_decode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out: Vec = Vec::new(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' && i + 2 < bytes.len() { + if let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) { + out.push(h * 16 + l); + i += 3; + continue; + } + } + out.push(bytes[i]); + i += 1; + } + String::from_utf8_lossy(&out).into_owned() + } + + // parse scheme://host:port/path?query#fragment , or the authority-less + // form scheme:path?query#fragment (mailto:, tel:, urn:, ...). + // Returns (scheme, hasAuthority, host, port, path, query, fragment). + fn parse_absolute(s: &str) -> (String, bool, String, i32, String, String, String) { + let idx = scheme_end(s).unwrap(); + let scheme = s[..idx].to_ascii_lowercase(); + let after = &s[idx + 1..]; // everything after "scheme:" + + match after.strip_prefix("//") { + Some(rest) => { + // scheme://authority/path?query#fragment + let auth_end = rest.find(|c| c == '/' || c == '?' || c == '#').unwrap_or(rest.len()); + let authority = &rest[..auth_end]; + let remainder = &rest[auth_end..]; + + let (host, port) = match authority.rfind(':') { + Some(ci) => { + let h = &authority[..ci]; + let p = &authority[ci + 1..]; + match p.parse::() { + Ok(n) => (h.to_ascii_lowercase(), n), + Err(_) => (authority.to_ascii_lowercase(), -1), + } + } + None => (authority.to_ascii_lowercase(), -1), + }; + + let (before_frag, fragment) = match remainder.find('#') { + Some(fi) => (&remainder[..fi], remainder[fi..].to_string()), + None => (remainder, String::new()), + }; + let (path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), + None => (before_frag.to_string(), String::new()), + }; + let path = if path.is_empty() { "/".to_string() } else { path }; + (scheme, true, host, port, path, query, fragment) + } + None => { + // authority-less: scheme:path?query#fragment (no host/port) + let (before_frag, fragment) = match after.find('#') { + Some(fi) => (&after[..fi], after[fi..].to_string()), + None => (after, String::new()), + }; + let (path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), + None => (before_frag.to_string(), String::new()), + }; + (scheme, false, String::new(), -1, path, query, fragment) + } + } + } + + fn parse_abs_uri(orig: &str) -> Uri { + let (scheme, has_authority, host, port, path, query, fragment) = parse_absolute(orig); + Uri { + isAbsolute: true, + hasAuthority: has_authority, + original: fromSlice(orig), + relative: string(""), + schemePart: fromString(scheme), + hostPart: fromString(host), + port, + pathPart: fromString(path), + queryPart: fromString(query), + fragmentPart: fromString(fragment), + } + } + + fn make_relative(orig: &str) -> Uri { + Uri { + isAbsolute: false, + hasAuthority: false, + original: fromSlice(orig), + relative: fromSlice(orig), + schemePart: string(""), + hostPart: string(""), + port: -1, + pathPart: string(""), + queryPart: string(""), + fragmentPart: string(""), + } + } + + // resolve a relative reference against an absolute base URI + fn resolve(base: &Uri, rel: &str) -> Uri { + if is_absolute_str(rel) { + return parse_abs_uri(rel); + } + let (before_frag, fragment) = match rel.find('#') { + Some(fi) => (&rel[..fi], rel[fi..].to_string()), + None => (rel, String::new()), + }; + let (rel_path, query) = match before_frag.find('?') { + Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), + None => (before_frag.to_string(), String::new()), + }; + let base_path = base.pathPart.as_str(); + let merged_path = if rel_path.is_empty() { + base_path.to_string() + } else if rel_path.starts_with('/') { + rel_path + } else { + let cut = base_path.rfind('/').map(|i| i + 1).unwrap_or(0); + format!("{}{}", &base_path[..cut], rel_path) + }; + // RFC 3986 §5.2.4: collapse `.`/`..` segments in the merged path. + let new_path = remove_dot_segments(&merged_path); + let scheme = base.schemePart.as_str(); + let host = base.hostPart.as_str(); + let auth = authority_str(scheme, host, base.port); + let orig = format!("{}://{}{}{}{}", scheme, auth, new_path, query, fragment); + Uri { + isAbsolute: true, + hasAuthority: base.hasAuthority, + original: fromString(orig), + relative: string(""), + schemePart: base.schemePart.clone(), + hostPart: base.hostPart.clone(), + port: base.port, + pathPart: fromString(new_path), + queryPart: fromString(query), + fragmentPart: fromString(fragment), + } + } + + fn build_from_string(orig: &str, kind: i32) -> Result { + let is_abs = is_absolute_str(orig); + match kind { + 1 => { + // Absolute + if is_abs { + Ok(parse_abs_uri(orig)) + } else { + Err("Invalid URI: The format of the URI could not be determined.") + } + } + 2 => { + // Relative + if is_abs { + Err("URI is not a relative path.") + } else { + Ok(make_relative(orig)) + } + } + _ => { + // RelativeOrAbsolute + if is_abs { + Ok(parse_abs_uri(orig)) + } else { + Ok(make_relative(orig)) + } + } + } + } + + // ------------------------------------------------------------------- + // instance surface + // ------------------------------------------------------------------- + + impl Uri { + fn ensure_absolute(&self) { + if !self.isAbsolute { + panic!("This operation is not supported for a relative URI."); + } + } + + fn build_absolute(&self) -> String { + let scheme = self.schemePart.as_str(); + if self.hasAuthority { + let auth = authority_str(scheme, self.hostPart.as_str(), self.port); + format!( + "{}://{}{}{}{}", + scheme, + auth, + self.pathPart.as_str(), + self.queryPart.as_str(), + self.fragmentPart.as_str() + ) + } else { + // authority-less scheme (mailto:, tel:, urn:): no `//host`. + format!( + "{}:{}{}{}", + scheme, + self.pathPart.as_str(), + self.queryPart.as_str(), + self.fragmentPart.as_str() + ) + } + } + + pub fn isAbsoluteUri(&self) -> bool { + self.isAbsolute + } + + pub fn scheme(&self) -> string { + self.ensure_absolute(); + self.schemePart.clone() + } + + pub fn host(&self) -> string { + self.ensure_absolute(); + self.hostPart.clone() + } + + pub fn absolutePath(&self) -> string { + self.ensure_absolute(); + self.pathPart.clone() + } + + pub fn query(&self) -> string { + self.ensure_absolute(); + self.queryPart.clone() + } + + pub fn fragment(&self) -> string { + self.ensure_absolute(); + self.fragmentPart.clone() + } + + pub fn pathAndQuery(&self) -> string { + self.ensure_absolute(); + fromString(format!("{}{}", self.pathPart.as_str(), self.queryPart.as_str())) + } + + pub fn absoluteUri(&self) -> string { + self.ensure_absolute(); + fromString(self.build_absolute()) + } + + pub fn originalString(&self) -> string { + self.original.clone() + } + } + + impl core::fmt::Display for Uri { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if self.isAbsolute { + write!(f, "{}", percent_decode(&self.build_absolute())) + } else { + write!(f, "{}", self.relative.as_str()) + } + } + } + + impl core::fmt::Debug for Uri { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::Display::fmt(self, f) + } + } + + // ------------------------------------------------------------------- + // constructors (free functions, mirror System.Uri overloads) + // ------------------------------------------------------------------- + + pub fn create(uriString: string) -> LrcPtr { + createWithKind(uriString, 1) + } + + pub fn createWithKind(uriString: string, kind: i32) -> LrcPtr { + match build_from_string(uriString.as_str(), kind) { + Ok(u) => LrcPtr::new(u), + Err(msg) => panic!("{}", msg), + } + } + + pub fn createFromString(baseUri: LrcPtr, relativeUri: string) -> LrcPtr { + if !baseUri.isAbsolute { + panic!("Invalid URI: A base URI cannot be created because the 'baseUri' parameter represents a relative URI."); + } + LrcPtr::new(resolve(&baseUri, relativeUri.as_str())) + } + + pub fn createFromUri(baseUri: LrcPtr, relativeUri: LrcPtr) -> LrcPtr { + if !baseUri.isAbsolute { + panic!("Invalid URI: A base URI cannot be created because the 'baseUri' parameter represents a relative URI."); + } + if relativeUri.isAbsolute { + // .NET: when the second URI is absolute it wins, and the base is ignored. + relativeUri.clone() + } else { + LrcPtr::new(resolve(&baseUri, relativeUri.relative.as_str())) + } + } + + pub fn tryCreateWithKind(uriString: string, kind: i32, res: &MutCell>) -> bool { + match build_from_string(uriString.as_str(), kind) { + Ok(u) => { + res.set(LrcPtr::new(u)); + true + } + Err(_) => false, + } + } + + pub fn tryCreateFromUri( + baseUri: LrcPtr, + relativeUri: LrcPtr, + res: &MutCell>, + ) -> bool { + if !baseUri.isAbsolute { + return false; + } + if relativeUri.isAbsolute { + // .NET: when the second URI is absolute it wins, and the base is ignored. + res.set(relativeUri.clone()); + } else { + res.set(LrcPtr::new(resolve(&baseUri, relativeUri.relative.as_str()))); + } + true + } + + // --- Percent-encoding (RFC 3986). Encoding operates on UTF-8 bytes. --- + + fn is_unreserved(b: u8) -> bool { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') + } + + // gen-delims and sub-delims, kept intact by EscapeUriString. + fn is_reserved(b: u8) -> bool { + matches!( + b, + b':' | b'/' | b'?' | b'#' | b'[' | b']' | b'@' + | b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' + ) + } + + fn escape_with(s: &str, keep: fn(u8) -> bool) -> string { + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + let mut out = String::with_capacity(s.len()); + for b in s.bytes() { + if keep(b) { + out.push(b as char); + } else { + out.push('%'); + out.push(HEX[(b >> 4) as usize] as char); + out.push(HEX[(b & 0x0F) as usize] as char); + } + } + fromString(out) + } + + pub fn escapeDataString(s: string) -> string { + escape_with(s.as_str(), is_unreserved) + } + + pub fn escapeUriString(s: string) -> string { + fn keep(b: u8) -> bool { + is_unreserved(b) || is_reserved(b) + } + escape_with(s.as_str(), keep) + } + + pub fn unescapeDataString(s: string) -> string { + let bytes = s.as_str().as_bytes(); + let mut out: Vec = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + // An invalid or truncated %XX sequence is left untouched, as in .NET. + if bytes[i] == b'%' && i + 2 < bytes.len() { + if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) { + out.push((hi << 4) | lo); + i += 3; + continue; + } + } + out.push(bytes[i]); + i += 1; + } + fromString(String::from_utf8_lossy(&out).into_owned()) + } +} diff --git a/src/fable-library-rust/src/lib.fs b/src/fable-library-rust/src/lib.fs index d207dc8467..fc92c08eee 100644 --- a/src/fable-library-rust/src/lib.fs +++ b/src/fable-library-rust/src/lib.fs @@ -14,18 +14,24 @@ let _imports () = importAll "./Decimal.rs" importAll "./Format.rs" importAll "./Encoding.rs" + importAll "./Environment.rs" + importAll "./Event.rs" importAll "./Exception.rs" + importAll "./File.rs" importAll "./Guid.rs" importAll "./HashMap.rs" importAll "./HashSet.rs" importAll "./Interop.rs" importAll "./Native.rs" importAll "./NativeArray.rs" + importAll "./Observable.rs" importAll "./Numeric.rs" + importAll "./Path.rs" importAll "./Random.rs" importAll "./Reflection.rs" importAll "./RegExp.rs" importAll "./String.rs" importAll "./TimeOnly.rs" importAll "./TimeSpan.rs" + importAll "./Uri.rs" () diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 5af3a4faef..9c4f9900ea 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -42,9 +42,10 @@ - + - + + @@ -58,10 +59,12 @@ - + + + @@ -71,6 +74,7 @@ + @@ -79,7 +83,7 @@ - + diff --git a/tests/Rust/tests/src/EnumerableTests.fs b/tests/Rust/tests/src/EnumerableTests.fs index e990afcc62..170c5998a6 100644 --- a/tests/Rust/tests/src/EnumerableTests.fs +++ b/tests/Rust/tests/src/EnumerableTests.fs @@ -25,17 +25,17 @@ let fibGen() = let mutable state = (1,1) { new IEnumerator<_> with member x.Current = (fst state) - interface IEnumerator with + interface IEnumerator with member x.Current = box (fst state) member x.MoveNext() = state <- (snd state, fst state + snd state); (fst state) < 1000 member x.Reset() = state <- (1,1) - interface System.IDisposable with + interface System.IDisposable with member x.Dispose() = () } let toSeq gen = { new IEnumerable<'T> with member x.GetEnumerator() = gen() - interface IEnumerable with + interface IEnumerable with member x.GetEnumerator() = (gen() :> IEnumerator) } let fib() = Seq.unfold (fun state -> diff --git a/tests/Rust/tests/src/EnvironmentTests.fs b/tests/Rust/tests/src/EnvironmentTests.fs new file mode 100644 index 0000000000..0d2437d45d --- /dev/null +++ b/tests/Rust/tests/src/EnvironmentTests.fs @@ -0,0 +1,22 @@ +// System.Environment reads process state, so the library module is std-only +// and these tests do not apply to no_std builds. +[] +module Fable.Tests.EnvironmentTests + +open Util.Testing +open System + +[] +let ``Environment.NewLine works`` () = + Environment.NewLine.Length > 0 + |> equal true + +[] +let ``Environment.GetEnvironmentVariable returns null for a missing variable`` () = + Environment.GetEnvironmentVariable("__FABLE_NON_EXISTENT_VAR__") + |> equal null + +[] +let ``Environment.CurrentDirectory works`` () = + Environment.CurrentDirectory.Length > 0 + |> equal true diff --git a/tests/Rust/tests/src/EventTests.fs b/tests/Rust/tests/src/EventTests.fs index 8575bb83eb..dde1561419 100644 --- a/tests/Rust/tests/src/EventTests.fs +++ b/tests/Rust/tests/src/EventTests.fs @@ -2,30 +2,44 @@ module Fable.Tests.EventTests open Util.Testing -type ClassWithCLIEvent() = - let event = new Event<_>() - [] - member _.Event = event.Publish - member this.TestEvent(arg) = - event.Trigger(this, arg) - -type ClassWithNonCLIEvent() = - let event = new Event<_>() - member _.Event = event.Publish - member this.TestEvent(arg) = - event.Trigger(this, arg) - -type InterfaceWithCLIEvent<'t> = - [] - abstract Event : IEvent,'t> - -type ClassWithInterfaceWithCLIEvent<'t>() = - let event = new Event<_,_>() - member this.TestEvent(arg) = - event.Trigger(this, arg) - interface InterfaceWithCLIEvent<'t> with - [] - member _.Event = event.Publish +// NOTE (Rust): these class-hosted event types are disabled on Rust. They trigger +// with `event.Trigger(this, arg)`, i.e. they pass the enclosing instance (`this`) +// as an event value. The Rust backend cannot recover the reference-counted `this` +// from a `&self` method body (it emits a by-value struct clone of the wrong type), +// and `member Event = event.Publish` exposes the event as `IEvent<_>`, which the +// backend does not coerce to `IObservable<_>` for `.Add`/`.Subscribe`. The plain +// `Event<'T>` module-function tests below (the core of this port) are unaffected. +// These pass on the JS/TS/Python targets. +// type ClassWithCLIEvent() = +// let event = new Event<_>() +// [] +// member _.Event = event.Publish +// member this.TestEvent(arg) = +// event.Trigger(this, arg) +// +// type ClassWithNonCLIEvent() = +// let event = new Event<_>() +// member _.Event = event.Publish +// member this.TestEvent(arg) = +// event.Trigger(this, arg) + +// NOTE (Rust): the two-generic Event<'Delegate,'Args> (FSharpEvent`2, with a +// sender-carrying 2-arg Trigger) and CLI events declared on an *interface* are +// not supported by the Rust runtime port yet. The `Microsoft.FSharp.Control` +// runtime only implements FSharpEvent`1 (single generic, 1-arg Trigger). These +// types and the two tests that use them (below) are therefore disabled on Rust; +// they pass on the JS/TS/Python targets. +// type InterfaceWithCLIEvent<'t> = +// [] +// abstract Event : IEvent,'t> +// +// type ClassWithInterfaceWithCLIEvent<'t>() = +// let event = new Event<_,_>() +// member this.TestEvent(arg) = +// event.Trigger(this, arg) +// interface InterfaceWithCLIEvent<'t> with +// [] +// member _.Event = event.Publish module tests = @@ -196,54 +210,61 @@ module tests = source.Trigger 6 equal 0 result - [] - let ``Classes can trigger CLI events`` () = - let mutable result = 0 - let classWithEvent = new ClassWithCLIEvent() - classWithEvent.Event.Add(fun (sender, x) -> result <- x) - classWithEvent.TestEvent(5) - equal 5 result - - [] - let ``Classes can trigger non-CLI events`` () = - let mutable result = "" - let classWithEvent = new ClassWithNonCLIEvent() - let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) - classWithEvent.TestEvent("Hello") - disp.Dispose() - classWithEvent.TestEvent("Bye") - equal "Hello" result - - [] - let ``Classes can trigger CLI events on interfaces`` () = - let mutable result = 0 - let mutable sender = null - let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() - (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> - sender <- s - result <- x - ) - classWithEvent.TestEvent(5) - equal 5 result - equal (box classWithEvent) sender - - [] - let ``Generic interface expression can have CLI events`` () = - let mutable actualSender = "" - let mutable result = false - let event = Event<_,_>() - let ifaceWIthEvent = - { new InterfaceWithCLIEvent<_> with - [] - member _.Event = event.Publish } - ifaceWIthEvent.Event.AddHandler(fun sender arg -> - actualSender <- string sender - result <- arg) - let expectedSender = "SENDER" - let expectedResult = true - event.Trigger(expectedSender, expectedResult) - equal expectedSender actualSender - equal expectedResult result + // NOTE (Rust): disabled — see the ClassWithCLIEvent / ClassWithNonCLIEvent + // note above (passing `this` as an event value + IEvent->IObservable coercion + // for `.Add`/`.Subscribe`). These pass on JS/TS/Python. + // [] + // let ``Classes can trigger CLI events`` () = + // let mutable result = 0 + // let classWithEvent = new ClassWithCLIEvent() + // classWithEvent.Event.Add(fun (sender, x) -> result <- x) + // classWithEvent.TestEvent(5) + // equal 5 result + // + // [] + // let ``Classes can trigger non-CLI events`` () = + // let mutable result = "" + // let classWithEvent = new ClassWithNonCLIEvent() + // let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) + // classWithEvent.TestEvent("Hello") + // disp.Dispose() + // classWithEvent.TestEvent("Bye") + // equal "Hello" result + + // NOTE (Rust): disabled — needs the two-generic Event<_,_> (FSharpEvent`2, + // 2-arg Trigger with sender) and CLI events declared on interfaces, which the + // Rust runtime port does not implement yet (see the commented types above). + // These pass on JS/TS/Python. + // [] + // let ``Classes can trigger CLI events on interfaces`` () = + // let mutable result = 0 + // let mutable sender = null + // let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() + // (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> + // sender <- s + // result <- x + // ) + // classWithEvent.TestEvent(5) + // equal 5 result + // equal (box classWithEvent) sender + // + // [] + // let ``Generic interface expression can have CLI events`` () = + // let mutable actualSender = "" + // let mutable result = false + // let event = Event<_,_>() + // let ifaceWIthEvent = + // { new InterfaceWithCLIEvent<_> with + // [] + // member _.Event = event.Publish } + // ifaceWIthEvent.Event.AddHandler(fun sender arg -> + // actualSender <- string sender + // result <- arg) + // let expectedSender = "SENDER" + // let expectedResult = true + // event.Trigger(expectedSender, expectedResult) + // equal expectedSender actualSender + // equal expectedResult result [] let ``Events are unsubscribed correctly`` () = // See #609 diff --git a/tests/Rust/tests/src/ObservableTests.fs b/tests/Rust/tests/src/ObservableTests.fs index 45b641f5cd..0ed72d768e 100644 --- a/tests/Rust/tests/src/ObservableTests.fs +++ b/tests/Rust/tests/src/ObservableTests.fs @@ -9,16 +9,35 @@ type MyObserver<'T>(f) = member x.OnError e = () member x.OnCompleted() = () +// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for two +// Rust-backend limitations (the runtime combinators themselves are unaffected): +// 1. The disposable is a named generic class, not an inline +// `{ new IDisposable with ... }` object expression: the Rust backend does not +// propagate the enclosing type's generic parameter into a generated +// object-expression struct (it emits an inner struct that references `T` +// without declaring it -> rustc E0401). +// 2. Subscriptions are keyed by an int id and removed by id, rather than +// `listeners.Remove(w)`: interface trait objects (`IObserver<'T>`) do not +// implement Rust `PartialEq`, so removal-by-value of an interface element is +// not supported. Behaviour is identical to the reference. +type private Unsubscriber<'T>(listeners: ResizeArray>, id: int) = + interface IDisposable with + member _.Dispose() = + let idx = listeners.FindIndex(fun (i, _) -> i = id) + if idx >= 0 then listeners.RemoveAt(idx) + type MyObservable<'T>() = - let listeners = ResizeArray>() + let listeners = ResizeArray>() + let mutable nextId = 0 member x.Trigger v = - for lis in listeners do + for (_, lis) in listeners do lis.OnNext v interface IObservable<'T> with member x.Subscribe w = - listeners.Add(w) - { new IDisposable with - member x.Dispose() = listeners.Remove(w) |> ignore } + let id = nextId + nextId <- nextId + 1 + listeners.Add((id, w)) + new Unsubscriber<'T>(listeners, id) :> IDisposable module tests = diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs new file mode 100644 index 0000000000..353a081eba --- /dev/null +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -0,0 +1,356 @@ +module Fable.Tests.QuotationTests + +open Util.Testing +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Linq.RuntimeHelpers + +// NOTE: quotations are deconstructed through an untyped `Expr` parameter (helpers below +// take `(e: Expr)`); matching a typed `Expr<'T>` binding directly is not yet supported on +// Rust (see quotation notes). Binding the boxed `obj` payload out of a Value node now works +// (the getZeroObj fix); the type element is bound as a wildcard since the runtime models it +// as a string rather than System.Type. Array-valued nodes now deconstruct: NewTuple exposes +// its elements, NewUnionCase binds a real UnionCaseInfo (uci.Name / uci.Tag) plus its args, and +// NewRecord binds its (erased) type slot plus its args. The NewRecord type slot and +// UnionCaseInfo.GetFields() are not modelled (System.Type / PropertyInfo have no Rust runtime). + +let private lambdaVarName (e: Expr) = + match e with + | Lambda(v, _body) -> v.Name + | _ -> "?" + +let private letVarName (e: Expr) = + match e with + | Let(v, _value, _body) -> v.Name + | _ -> "?" + +let private isIfThenElseNode (e: Expr) = + match e with + | IfThenElse(_g, _t, _e) -> true + | _ -> false + +[] +let ``Evaluate a value`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 42 @> + unbox r |> equal 42 + +// Typed Expr<'T> matched directly (not routed through an (e: Expr) helper): the type +// argument is erased so the quotation shares the untyped FSharpExpr representation. + +[] +let ``Typed quotation matches Value directly`` () = + match <@ 42 @> with + | Value(v, _) -> unbox v |> equal 42 + | _ -> failwith "Expected Value" + +[] +let ``Typed quotation matches Lambda directly`` () = + match <@ fun (x: int) -> x + 1 @> with + | Lambda(v, _) -> v.Name |> equal "x" + | _ -> failwith "Expected Lambda" + +[] +let ``Lambda quotation exposes the parameter name`` () = + lambdaVarName <@ fun (x: int) -> x + 1 @> |> equal "x" + +[] +let ``Let quotation exposes the bound name`` () = + letVarName <@ let y = 5 in y + 1 @> |> equal "y" + +[] +let ``IfThenElse quotation deconstructs`` () = + isIfThenElseNode <@ if true then 1 else 2 @> |> equal true + +[] +let ``Evaluate addition`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 2 + 3 @> + unbox r |> equal 5 + +[] +let ``Evaluate subtraction`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 10 - 4 @> + unbox r |> equal 6 + +[] +let ``Evaluate comparison`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 5 > 3 @> + unbox r |> equal true + +[] +let ``Evaluate let binding`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let x = 10 in x + 5 @> + unbox r |> equal 15 + +[] +let ``Evaluate lambda application`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ (fun a -> a * 2) 21 @> + unbox r |> equal 42 + +[] +let ``Evaluate let-bound lambda`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let f = (fun x -> x + 1) in f 41 @> + unbox r |> equal 42 + +// --- Parity expansion (toward the Python 27) --- +// Binding the boxed obj payload out of a Value node previously tripped getZero on Rust; +// the getZeroObj / active-pattern fixes should now allow these. Type element bound as +// wildcard (`Value(v, _)`) to sidestep the System.Type-vs-string model difference. + +let private valueInt (e: Expr) = + match e with + | Value(v, _) -> unbox v + | _ -> -1 + +let private valueBool (e: Expr) = + match e with + | Value(v, _) -> unbox v + | _ -> false + +let private valueStr (e: Expr) = + match e with + | Value(v, _) -> unbox v + | _ -> "?" + +let private letBoundInt (e: Expr) = + match e with + | Let(_, Value(v, _), _) -> unbox v + | _ -> -1 + +let private seqSecondInt (e: Expr) = + match e with + | Sequential(_, Value(v, _)) -> unbox v + | _ -> -1 + +let private newTupleLen (e: Expr) = + match e with + | NewTuple(exprs) -> List.length exprs + | _ -> -1 + +let private isAppLambdaValue (e: Expr) = + match e with + | Application(Lambda _, Value _) -> true + | _ -> false + +let private lambdaVarBodyName (e: Expr) = + match e with + | Lambda(v, Var v2) -> v.Name + "/" + v2.Name + | _ -> "?" + +let private isLambdaIfThenElse (e: Expr) = + match e with + | Lambda(_, IfThenElse(_, _, _)) -> true + | _ -> false + +[] +let ``Value node binds int payload`` () = + valueInt <@ 42 @> |> equal 42 + +[] +let ``Value node binds bool payload`` () = + valueBool <@ true @> |> equal true + +[] +let ``Value node binds string payload`` () = + valueStr <@ "hello" @> |> equal "hello" + +[] +let ``Let binds value node payload`` () = + letBoundInt <@ let x = 5 in x @> |> equal 5 + +[] +let ``Sequential exposes second value payload`` () = + seqSecondInt <@ (); 42 @> |> equal 42 + +[] +let ``NewTuple exposes element count`` () = + newTupleLen <@ (1, 2, 3) @> |> equal 3 + +[] +let ``Application of lambda to value deconstructs`` () = + isAppLambdaValue <@ (fun x -> x) 42 @> |> equal true + +[] +let ``Lambda body Var refers to the parameter`` () = + lambdaVarBodyName <@ fun x -> x @> |> equal "x/x" + +[] +let ``Option match in quotation lowers to IfThenElse`` () = + isLambdaIfThenElse <@ fun (o: int option) -> match o with Some v -> v | None -> 0 @> |> equal true + +[] +let ``Literal match in quotation lowers to IfThenElse`` () = + isLambdaIfThenElse <@ fun (x: int) -> match x with 0 -> "zero" | _ -> "other" @> |> equal true + +[] +let ``Evaluate multiplication`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ 6 * 7 @> + unbox r |> equal 42 + +[] +let ``Evaluate nested let bindings`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let x = 3 in let y = 4 in x * y @> + unbox r |> equal 12 + +[] +let ``Evaluate nested lambda`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> + unbox r |> equal 7 + +// --- NewUnionCase / NewRecord deconstruction --- + +type private MyRec = { X: int; Y: int } + +let private newUnionCaseName (e: Expr) = + match e with + | NewUnionCase(uci, _args) -> uci.Name + | _ -> "?" + +let private newUnionCaseArgCount (e: Expr) = + match e with + | NewUnionCase(_uci, args) -> List.length args + | _ -> -1 + +let private newUnionCaseTag (e: Expr) = + match e with + | NewUnionCase(uci, _args) -> uci.Tag + | _ -> -1 + +let private newRecordArgCount (e: Expr) = + match e with + | NewRecord(_t, args) -> List.length args + | _ -> -1 + +[] +let ``NewUnionCase exposes the case name`` () = + newUnionCaseName <@ Some 42 @> |> equal "Some" + +[] +let ``NewUnionCase exposes the argument count`` () = + newUnionCaseArgCount <@ Some 42 @> |> equal 1 + +[] +let ``NewUnionCase exposes the tag`` () = + newUnionCaseTag <@ Some 42 @> |> equal 1 + +[] +let ``NewRecord exposes the argument count`` () = + newRecordArgCount <@ ({ X = 1; Y = 2 }: MyRec) @> |> equal 2 + +// --- TupleGet evaluation (B3) --- +// A tuple-deconstructing let lowers to a NewTuple bound to a var, then TupleGet nodes +// pulling out each element. Evaluating it exercises both the ExprNewTuple arm (builds a +// boxed obj[]) and the ExprTupleGet arm (indexes into it). Before the fix, TupleGet +// returned the whole tuple, so `a`/`b` were the obj[] and `a + b` would not evaluate. + +[] +let ``Evaluate tuple-get picks the element at the index`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let (a, b) = (10, 20) in a + b @> + unbox r |> equal 30 + +[] +let ``Evaluate tuple-get picks the second element`` () = + let r = LeafExpressionConverter.EvaluateQuotation <@ let (a, b, c) = (1, 2, 3) in b @> + unbox r |> equal 2 + +// --- Call deconstruction with MethodInfo (B1) --- +// Deconstructing a Call now binds a real MethodInfo: mi.Name is the compiled method name and +// mi.DeclaringType.FullName is the declaring module fullname. The declaring type is the +// SQLProvider linchpin: it distinguishes List.map from Array.map. MethodInfo erases to the +// Rust-native FSharpMethodInfo carrier; DeclaringType is the fullname string boxed as +// System.Type (System.Type erases to Any), read back via Reflection.fullName. + +let private callMethodName (e: Expr) = + match e with + | Call(_, mi, _) -> mi.Name + | _ -> "?" + +let private callDeclaringTypeName (e: Expr) = + match e with + | Call(_, mi, _) -> mi.DeclaringType.FullName + | _ -> "?" + +[] +let ``Call exposes the method name`` () = + callMethodName <@ List.map (fun x -> x + 1) [ 1 ] @> |> equal "Map" + +[] +let ``Call exposes the declaring type full name`` () = + callDeclaringTypeName <@ List.map (fun x -> x + 1) [ 1 ] @> + |> equal "Microsoft.FSharp.Collections.ListModule" + +[] +let ``Call distinguishes List.map from Array.map by declaring type`` () = + callDeclaringTypeName <@ Array.map (fun x -> x + 1) [| 1 |] @> + |> equal "Microsoft.FSharp.Collections.ArrayModule" + +[] +let ``Call binds MethodInfo directly`` () = + match <@ List.map (fun x -> x + 1) [ 1 ] @> with + | Call(_, mi, _) -> + mi.Name |> equal "Map" + mi.DeclaringType.FullName |> equal "Microsoft.FSharp.Collections.ListModule" + | _ -> failwith "Expected Call" + +// --- PropertyGet (isFieldGet) --- +// A property getter deconstructs as PropertyGet(instance, propInfo, indexerArgs), matching +// real F# quotations and the JS/TS/Python runtimes. System.Reflection.PropertyInfo erases to +// the shared FSharpPropertyInfo carrier, so pi.Name works here exactly as it does for the +// PropertyInfo values returned by FSharpType.GetRecordFields. + +type QuotPropHolder = + static member Version = 42 + +[] +let ``Option.Value access inside a quotation is a PropertyGet`` () = + let q = <@ fun (o: int option) -> o.Value @> + + match q with + | Lambda(_, PropertyGet(Some _, _, [])) -> () + | _ -> failwith "Expected Lambda with PropertyGet body" + +[] +let ``List.Head access inside a quotation is a PropertyGet`` () = + let q = <@ fun (xs: int list) -> xs.Head @> + + match q with + | Lambda(_, PropertyGet(Some _, _, [])) -> () + | _ -> failwith "Expected Lambda with PropertyGet body" + +// A static property carries the "novalue" no-instance sentinel as its target, which must +// surface as None rather than Some sentinelNode. +[] +let ``Static property access inside a quotation is a PropertyGet with no instance`` () = + let q = <@ QuotPropHolder.Version @> + + match q with + | PropertyGet(None, _, []) -> () + | _ -> failwith "Expected PropertyGet with no instance" + +[] +let ``PropertyGet exposes the property name`` () = + let q = <@ fun (o: int option) -> o.Value @> + + match q with + | Lambda(_, PropertyGet(_, pi, _)) -> pi.Name |> equal "Value" + | _ -> failwith "Expected Lambda with PropertyGet body" + +// --- Call instance slot (isCall) --- +// A static call carries the "novalue" no-instance sentinel as its instance, which must +// surface as None rather than Some sentinelNode — the isCall counterpart of the static +// PropertyGet test above. The instance case proves the sentinel is not over-applied. + +[] +let ``Static call inside a quotation has no instance`` () = + let q = <@ List.length [ 1; 2; 3 ] @> + + match q with + | Call(None, _mi, args) -> equal 1 (List.length args) + | _ -> failwith "Expected a static Call with no instance" + +[] +let ``Instance call inside a quotation has an instance`` () = + let q = <@ fun (s: string) -> s.Trim() @> + + match q with + | Lambda(_, Call(Some _, _mi, _)) -> () + | _ -> failwith "Expected Lambda with an instance Call body" diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs new file mode 100644 index 0000000000..bfa7462820 --- /dev/null +++ b/tests/Rust/tests/src/RecordReflectionTests.fs @@ -0,0 +1,125 @@ +// Value-based reflection needs the type registry, which is std-only +// (it uses thread_local), so these tests do not apply to no_std builds. +[] +module Fable.Tests.RecordReflectionTests + +open Util.Testing +open Microsoft.FSharp.Reflection + +// Record reflection MVP for the Rust target. Mirrors the record-focused cases of +// the (disabled) ReflectionTests.fs / JS Main ReflectionTests.fs. Value comparisons +// are done after unbox<'T> rather than on boxed `obj` directly, because Rust has no +// structural equality on `dyn Any` (see report). Behaviour is otherwise equivalent. + +type TestRecord = { String: string; Int: int } +type OtherRecord = { Z: int } + +type SampleUnion = + | CaseA + | CaseB of int + +[] +let ``typeof and System.Type equality on records`` () = + (typeof = typeof) |> equal true + (typeof = typeof) |> equal false + +[] +let ``typeof and System.Type equality on non-record types`` () = + // A non-record `typeof` must materialize as a valid runtime value (a boxed + // TypeId), and equality must distinguish types rather than always being equal. + let t = typeof + ignore t + (typeof = typeof) |> equal true + (typeof = typeof) |> equal false + (typeof = typeof) |> equal true + (typeof = typeof) |> equal false + // Mixed record / non-record must not compare equal. + (typeof = typeof) |> equal false + +[] +let ``FSharpType.IsRecord works`` () = + FSharpType.IsRecord(typeof) |> equal true + +[] +let ``FSharpType.GetRecordFields exposes PropertyInfo names`` () = + let fields = FSharpType.GetRecordFields(typeof) + fields.Length |> equal 2 + fields.[0].Name |> equal "String" + fields.[1].Name |> equal "Int" + +[] +let ``FSharpValue.GetRecordFields returns boxed field values`` () = + // Value-based reflection resolves the type via the runtime registry, which is + // populated when typeof is evaluated (as the JS record test also does first). + FSharpType.IsRecord(typeof) |> equal true + let record = { String = "a"; Int = 1 } + let values = FSharpValue.GetRecordFields(record) + values.Length |> equal 2 + unbox values.[0] |> equal "a" + unbox values.[1] |> equal 1 + +[] +let ``FSharpValue.GetRecordField reads an individual field`` () = + let record = { String = "a"; Int = 1 } + let fields = FSharpType.GetRecordFields(typeof) + unbox (FSharpValue.GetRecordField(record, fields.[0])) |> equal "a" + unbox (FSharpValue.GetRecordField(record, fields.[1])) |> equal 1 + +[] +let ``PropertyInfo.GetValue reads a field from a boxed record`` () = + let record = { String = "a"; Int = 1 } + let fields = FSharpType.GetRecordFields(typeof) + unbox (fields.[0].GetValue(box record)) |> equal "a" + unbox (fields.[1].GetValue(box record)) |> equal 1 + +[] +let ``FSharpValue.MakeRecord round-trips`` () = + let record = { String = "a"; Int = 1 } + let values = FSharpValue.GetRecordFields(record) + let rebuilt = unbox (FSharpValue.MakeRecord(typeof, values)) + rebuilt |> equal record + +[] +let ``FSharp.Reflection: Record (end to end)`` () = + let record = { String = "a"; Int = 1 } + let recordTypeFields = FSharpType.GetRecordFields(typeof) + let recordValueFields = FSharpValue.GetRecordFields(record) + + FSharpType.IsRecord(typeof) |> equal true + recordTypeFields.Length |> equal 2 + + // field names line up with values (verified per-slot, unboxed) + recordTypeFields.[0].Name |> equal "String" + unbox recordValueFields.[0] |> equal "a" + recordTypeFields.[1].Name |> equal "Int" + unbox recordValueFields.[1] |> equal 1 + + // GetRecordField matches GetRecordFields per slot + unbox (FSharpValue.GetRecordField(record, recordTypeFields.[0])) |> equal "a" + unbox (FSharpValue.GetRecordField(record, recordTypeFields.[1])) |> equal 1 + + // MakeRecord reconstructs an equal record + unbox (FSharpValue.MakeRecord(typeof, recordValueFields)) |> equal record + +[] +let ``obj.GetType round-trips to the concrete record type`` () = + let record = { String = "a"; Int = 1 } + FSharpType.IsRecord(typeof) |> equal true // ensure registration + ((box record).GetType() = typeof) |> equal true + let rebuilt = + unbox (FSharpValue.MakeRecord((box record).GetType(), FSharpValue.GetRecordFields(record))) + rebuilt |> equal record + +// box_lrc/unbox_lrc coerce the same smart pointer rather than cloning the value, +// so a record boxed to obj and unboxed again is physically the same instance. +[] +let ``Boxing a record to obj and unboxing preserves reference identity`` () = + let r = + { + String = "a" + Int = 1 + } + + let o: obj = box r + let r2 = unbox o + System.Object.ReferenceEquals(r, r2) |> equal true diff --git a/tests/Rust/tests/src/SystemIOTests.fs b/tests/Rust/tests/src/SystemIOTests.fs new file mode 100644 index 0000000000..4fcfadb38c --- /dev/null +++ b/tests/Rust/tests/src/SystemIOTests.fs @@ -0,0 +1,83 @@ +// System.IO needs a filesystem, so the library modules are std-only and +// these tests do not apply to no_std builds. +[] +module Fable.Tests.SystemIOTests + +open Util.Testing +open System.IO + +[] +let ``Path.Combine works with two parts`` () = + let combined = Path.Combine("foo", "bar") + Path.GetFileName(combined) |> equal "bar" + Path.GetDirectoryName(combined) |> equal "foo" + +[] +let ``Path.Combine works with three parts`` () = + let combined = Path.Combine("foo", "bar", "baz.txt") + Path.GetFileName(combined) |> equal "baz.txt" + Path.GetDirectoryName(combined) |> equal (Path.Combine("foo", "bar")) + +[] +let ``Path.Combine with a rooted second part uses the second part`` () = + // .NET: a rooted later argument discards the earlier one. + Path.Combine("/foo", "/bar") |> equal "/bar" + Path.Combine("foo", "/bar") |> equal "/bar" + +[] +let ``Path.GetDirectoryName works`` () = + Path.GetDirectoryName("temp/test.txt") + |> equal "temp" + +[] +let ``Path.GetExtension works`` () = + Path.GetExtension("temp/test.txt") + |> equal ".txt" + +[] +let ``Path.GetFileName works`` () = + Path.GetFileName("temp/test.txt") + |> equal "test.txt" + +[] +let ``Path.GetFileNameWithoutExtension works`` () = + Path.GetFileNameWithoutExtension("temp/test.txt") + |> equal "test" + +[] +let ``Path.HasExtension works`` () = + Path.HasExtension("temp/test.txt") + |> equal true + + Path.HasExtension("temp/test") + |> equal false + +[] +let ``Path.GetTempPath works`` () = + (Path.GetTempPath()).Length > 0 + |> equal true + +[] +let ``Path.GetRandomFileName works`` () = + (Path.GetRandomFileName()).Length > 0 + |> equal true + +[] +let ``File round-trip works`` () = + let file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + File.Exists(file) |> equal false + + File.WriteAllText(file, "Hello World") + File.Exists(file) |> equal true + + File.ReadAllText(file) + |> equal "Hello World" + + File.Delete(file) + File.Exists(file) |> equal false + +[] +let ``File.Delete on a missing file is a no-op`` () = + // .NET semantics: deleting a non-existent file does not throw. + File.Delete("definitely-missing-file-xyz.tmp") + true |> equal true diff --git a/tests/Rust/tests/src/UriTests.fs b/tests/Rust/tests/src/UriTests.fs index d3c6eb5526..f6ac81de32 100644 --- a/tests/Rust/tests/src/UriTests.fs +++ b/tests/Rust/tests/src/UriTests.fs @@ -85,6 +85,15 @@ let ``Uri from baseUri with absolute Uri works`` () = equal "www.example.com" uri.Host equal "http://www.example.com/" uri.AbsoluteUri +[] +let ``Uri from baseUri with a different absolute Uri yields the absolute one`` () = + // .NET: when the second URI is absolute it wins and the base is ignored. + let baseUri = Uri("http://www.base.com/", UriKind.Absolute) + let absUri = Uri("http://www.other.com/path", UriKind.Absolute) + let uri = Uri(baseUri, absUri) + equal "www.other.com" uri.Host + equal "http://www.other.com/path" uri.AbsoluteUri + [] let ``TryCreate from absolute string with kind Absolute works`` () = let (valid, uri) = Uri.TryCreate("http://www.test0.com/hello?a=b#c", UriKind.Absolute) @@ -140,6 +149,15 @@ let ``TryCreate from absolute uri and absolute uri should work`` () = equal true valid equal absUri uri +[] +let ``TryCreate from absolute uri and different absolute uri yields the second`` () = + // .NET: when the second URI is absolute it wins and the base is ignored. + let baseUri = Uri("http://www.base.com/", UriKind.Absolute) + let other = Uri("http://www.other.com/path", UriKind.Absolute) + let (valid, uri) = Uri.TryCreate(baseUri, other) + equal true valid + equal "www.other.com" uri.Host + [] let ``TryCreate from absolute uri and relative uri should work`` () = let (valid, uri) = Uri.TryCreate(Uri("https://example.com", UriKind.Absolute), Uri("test", UriKind.Relative)) @@ -163,6 +181,27 @@ let ``Uri.ToString works`` () = uri.ToString() |> equal "http://www.test4.com/a b c.html" sprintf "%A" uri |> equal "http://www.test4.com/a b c.html" +[] +let ``Uri from authority-less absolute scheme works`` () = + // mailto:/tel:/urn: are absolute URIs without an authority; must not panic. + let mailto = Uri("mailto:a@b.com") + equal true mailto.IsAbsoluteUri + equal "mailto" mailto.Scheme + equal "mailto:a@b.com" mailto.AbsoluteUri + let tel = Uri("tel:+1") + equal true tel.IsAbsoluteUri + equal "tel" tel.Scheme + let urn = Uri("urn:x:y") + equal true urn.IsAbsoluteUri + equal "urn" urn.Scheme + +[] +let ``Uri relative resolution collapses dot segments`` () = + let uri = Uri(Uri("http://h/a/b"), "../c") + equal "http://h/c" uri.AbsoluteUri + let uri2 = Uri(Uri("http://h/a/b/"), "../../x") + equal "http://h/x" uri2.AbsoluteUri + [] let ``Uri.OriginalString works`` () = let cases = [ @@ -174,3 +213,21 @@ let ``Uri.OriginalString works`` () = for case in cases do let uri = Uri(case) uri.OriginalString |> equal case + +[] +let ``Uri.EscapeDataString works`` () = + Uri.EscapeDataString("Hello World!") |> equal "Hello%20World%21" + Uri.EscapeDataString("aA1-_.~") |> equal "aA1-_.~" + // multi-byte chars percent-encode each UTF-8 byte + Uri.EscapeDataString("ä") |> equal "%C3%A4" + +[] +let ``Uri.UnescapeDataString works`` () = + Uri.UnescapeDataString("Hello%20World%21") |> equal "Hello World!" + Uri.UnescapeDataString("%C3%A4") |> equal "ä" + // an invalid escape sequence is left untouched + Uri.UnescapeDataString("100%") |> equal "100%" + +[] +let ``Uri.EscapeUriString keeps reserved characters`` () = + Uri.EscapeUriString("http://test.com/a b?c=d&e") |> equal "http://test.com/a%20b?c=d&e" From ca165045703797f0558da4bc6e06cbea7af231de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:26:08 -0700 Subject: [PATCH 003/322] fix(rust): Observables and Events cleanup (#4908) --- .github/workflows/build.yml | 4 +- src/Fable.Build/Main.fs | 5 +- src/Fable.Build/Test/Rust.fs | 47 ++- src/Fable.Transforms/Rust/Fable2Rust.fs | 369 ++++++++++++++++-- src/Fable.Transforms/Rust/Replacements.fs | 13 +- src/fable-library-rust/src/Event.rs | 100 ++++- src/fable-library-rust/src/Native.rs | 49 ++- src/fable-library-rust/src/Observable.rs | 230 +++++++---- src/fable-library-rust/src/Path.rs | 15 +- src/fable-library-rust/src/Quotation.fs | 290 +++++++++++++- src/fable-library-rust/src/Reflection.rs | 159 +++++++- src/fable-library-rust/src/TimeSpan.rs | 17 +- src/fable-library-rust/src/Uri.rs | 124 +++++- tests/Rust/Cargo.toml | 1 + tests/Rust/tests/src/EventTests.fs | 153 +++++--- tests/Rust/tests/src/ObservableTests.fs | 99 ++++- tests/Rust/tests/src/QuotationTests.fs | 51 +++ tests/Rust/tests/src/RecordReflectionTests.fs | 23 +- tests/Rust/tests/src/SystemIOTests.fs | 15 + tests/Rust/tests/src/UriTests.fs | 33 ++ 20 files changed, 1520 insertions(+), 277 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5aae1394f5..cc565590d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -272,7 +272,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - test: ["default", "no_std", "threaded"] + features: [",", "no_std", "threaded", "lrc_ptr"] steps: - uses: actions/checkout@v7.0.1 @@ -296,7 +296,7 @@ jobs: run: ./build.sh fable-library --rust - name: Fable Tests - Rust - run: ./build.sh test rust --${{ matrix.test }} + run: ./build.sh test rust --features "${{ matrix.features }}" # Separate build job for Dart build-dart: diff --git a/src/Fable.Build/Main.fs b/src/Fable.Build/Main.fs index d2df0287da..4473cda2cb 100644 --- a/src/Fable.Build/Main.fs +++ b/src/Fable.Build/Main.fs @@ -63,8 +63,9 @@ Available commands: Options for Rust: --ast-only Run only the tests for the AST (can be run in watch mode) - --no_std Compile and run the tests without the standard library - --threaded Compile and run the tests with the threaded runtime + --no_std Legacy alias for --features no_std + --threaded Legacy alias for --features threaded + --features Compile and run with comma-separated features; omit or use "," for default features Options for Python: --type-check Run type checking on the generated code with Pyright diff --git a/src/Fable.Build/Test/Rust.fs b/src/Fable.Build/Test/Rust.fs index e37805cdbe..e4222c197c 100644 --- a/src/Fable.Build/Test/Rust.fs +++ b/src/Fable.Build/Test/Rust.fs @@ -27,12 +27,37 @@ let handle (args: string list) = let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let astOnly = args |> List.contains "--ast-only" - let noStd = args |> List.contains "--no_std" - let threaded = args |> List.contains "--threaded" let noDotnet = args |> List.contains "--no-dotnet" + let requestedFeatures = + match args |> List.tryFindIndex ((=) "--features") with + | Some index -> + args + |> List.tryItem (index + 1) + |> Option.defaultValue "," + |> fun features -> + features.Split([| ',' |], System.StringSplitOptions.RemoveEmptyEntries) + |> Array.map (fun feature -> feature.Trim()) + |> Array.filter (fun feature -> feature <> "") + |> Array.toList + | None -> [] + + let legacyFeatures = + [ "--no_std", "no_std"; "--threaded", "threaded" ] + |> List.choose (fun (flag, feature) -> + if args |> List.contains flag then + Some feature + else + None + ) + + let features = requestedFeatures @ legacyFeatures |> List.distinct + + let noStd = features |> List.contains "no_std" + let threaded = features |> List.contains "threaded" + if noStd && threaded then - failwith "Cannot use --no-std and --threaded at the same time" + failwith "Cannot use no_std and threaded features together" BuildFableLibraryRust().Run(forceFableLibrary) @@ -58,13 +83,19 @@ let handle (args: string list) = Shell.copyFile destination file ) + let cargoFeatures = features |> String.concat "," + let cargoTestArgs = - if noStd then - "cargo test --features no_std -- --test-threads=1" - elif threaded then - "cargo test --features threaded" + let cargoTest = + if List.isEmpty features then + "cargo test" + else + $"cargo test --features {cargoFeatures}" + + if threaded then + cargoTest else - "cargo test -- --test-threads=1" + $"{cargoTest} -- --test-threads=1" let fableArgs = CmdLine.concat diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 015e7d81e7..3a2a3d2893 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -55,7 +55,9 @@ type Context = ScopedSymbols: FSharp.Collections.Map // HasMultipleUses: bool //this could be a closure in a map, or a for loop. The point is anything leaving the scope cannot be assumed to be the only reference InferAnyType: bool + SkipRecordTypeRegistration: bool IsAssocMember: bool + IsFluentMember: bool IsLambda: bool IsParamByRefPreferred: bool RequiresSendSync: bool // a way to implicitly propagate Arc's down the hierarchy when it is not possible to explicitly tag @@ -626,6 +628,14 @@ module TypeInfo = let ent = com.GetEntity(entRef) (ent.IsFSharpRecord || ent.IsFSharpUnion) && not ent.IsValueType + let isReferenceClass (com: IRustCompiler) (entRef: Fable.EntityRef) = + let ent = com.GetEntity(entRef) + + not ent.IsInterface + && not ent.IsValueType + && not ent.IsFSharpRecord + && not ent.IsFSharpUnion + // Checks whether the type needs a ref counted wrapper // such as Rc (or Arc in a multithreaded context) let shouldBeRefCountWrapped (com: IRustCompiler) ctx typ = @@ -1189,6 +1199,8 @@ module TypeInfo = // concrete type must resolve to Event_::FSharpEvent`1 as well. | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`1") (_, [ t ]) -> transformImportType com ctx [ t ] "Event" "FSharpEvent`1" + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`2") (_, [ _delegateType; argsType ]) -> + transformImportType com ctx [ argsType ] "Event" "FSharpEvent`2" // implemented regex types | Replacements.Util.IsEntity (Types.regexMatch) (_, []) -> transformImportType com ctx [] "RegExp" "Match" @@ -1626,22 +1638,42 @@ module Util = let ar = makeLibCall com ctx None "HashMap" "entries" [ expr ] makeLibCall com ctx None "Seq" "ofArray" [ ar ] - // boxing value types or wrapped types - | t, Fable.Any when isValueType com t || isWrappedType com t -> expr |> boxValue com ctx + // Boxing reads a value through an owned Rust expression. Clone first so fields + // read from reference-counted records are not moved out of their owner. + | t, Fable.Any when isValueType com t || isWrappedType com t -> expr |> makeClone |> boxValue com ctx // unboxing value types or wrapped types | Fable.Any, t when isValueType com t || isWrappedType com t -> expr |> unboxValue com ctx t - // boxing a reference-typed record/union (Lrc-wrapped) to obj: coerce the - // smart pointer to `dyn Any` so its concrete pointee stays the struct. - | Fable.DeclaredType(entRef, _), Fable.Any when isReferenceRecordOrUnion com entRef -> - [ expr ] |> makeLibCall com ctx None "Native" "box_lrc" + // Boxing a reference-typed record/union must use the same pointer representation + // as the source value. The LrcPtr case preserves identity; the explicit pointer + // attributes use a cloned underlying value so the object still carries its record + // TypeId even when Rc/Arc/Box cannot be unsized to the target object pointer. + | Fable.DeclaredType(entRef, genArgs), Fable.Any when isReferenceRecordOrUnion com entRef -> + let boxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "box_lrc" + | Some Rc -> "box_rc" + | Some Arc -> "box_arc" + | Some Box -> "box_box" + | None -> "box_" + + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" boxMethod // unboxing obj back to a reference-typed record/union: downcast + re-wrap. | Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef -> let rawTy = transformEntityType com ctx entRef genArgs let genArgsOpt = [ rawTy ] |> mkTypesGenericArgs - [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox_lrc" + + let unboxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "unbox_lrc" + | Some Rc -> "unbox_rc" + | Some Arc -> "unbox_arc" + | Some Box -> "unbox_box" + | None -> "unbox" + + [ expr ] |> makeLibCall com ctx genArgsOpt "Native" unboxMethod // casts to generic param | _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value) @@ -1649,6 +1681,12 @@ module Util = // casts to IDictionary, for now does nothing // TODO: fix it | Replacements.Util.IsEntity (Types.dictionary) _, Replacements.Util.IsEntity (Types.idictionary) _ -> expr + // IEvent inherits IObservable in F#, but the generated Rust traits are flattened + // rather than related by Rust trait inheritance. Adapt the event view explicitly. + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.IEvent`2") (_, [ _; eventType ]), + Replacements.Util.IsEntity ("System.IObservable`1") (_, [ observableType ]) when eventType = observableType -> + makeLibCall com ctx None "Event" "asObservable" [ expr ] + // casts from object to interface | t1, t2 when not (isInterface com t1) && (isInterface com t2) -> makeInterfaceCast com ctx t2 expr @@ -1794,7 +1832,35 @@ module Util = let ctx = { ctx with IsParamByRefPreferred = isByRefPreferred || ctx.IsParamByRefPreferred } - transformLeaveContext com ctx argType arg + match argType, arg.Type with + | Some Fable.Any, actualType -> + match arg, actualType with + | Fable.IdentExpr ident, _ when ident.IsThisArgument && ctx.IsAssocMember -> + let expr = transformLeaveContext com ctx (Some Fable.Any) arg + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_lrc" + | MaybeCasted(Fable.IdentExpr ident), _ when ident.IsThisArgument && ctx.IsAssocMember -> + let expr = transformLeaveContext com ctx (Some Fable.Any) arg + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_lrc" + | _, Fable.DeclaredType(entRef, genArgs) when isReferenceClass com entRef -> + let expr = transformLeaveContext com ctx (Some Fable.Any) arg + + let boxMethod = + match shouldBeRefCountWrapped com ctx (Fable.DeclaredType(entRef, genArgs)) with + | Some Lrc -> "box_lrc" + | Some Rc -> "box_rc" + | Some Arc -> "box_arc" + | Some Box -> "box_box" + | None -> "box_" + + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" boxMethod + | _, actualType when actualType <> Fable.Any -> transformCast com ctx Fable.Any arg + | _ -> transformLeaveContext com ctx (Some Fable.Any) arg + | Some(Replacements.Util.IsEntity ("System.IObservable`1") (_, [ observableType ])), + Replacements.Util.IsEntity ("Microsoft.FSharp.Control.IEvent`2") (_, [ _; eventType ]) when + observableType = eventType + -> + transformCast com ctx observableType arg + | _ -> transformLeaveContext com ctx argType arg ) let makeRefForPatternMatch (com: IRustCompiler) ctx typ (nameOpt: string option) fableExpr = @@ -1919,11 +1985,6 @@ module Util = makeLibCall com ctx None "Native" "is_null" [ value |> mkAddrOfExpr ] let makeNull com ctx (typ: Fable.Type) = - let typ = - match typ with - | Fable.Any -> Fable.Unit - | t -> t - let genArgsOpt = transformGenArgs com ctx [ typ ] makeLibCall com ctx genArgsOpt "Native" "null" [] @@ -2077,7 +2138,15 @@ module Util = let entName = getEntityFullName com ctx entRef let path = makeFullNamePath entName genArgsOpt let expr = mkStructExpr path fields // TODO: range - expr |> maybeWrapSmartPtr com ctx ent + let expr = expr |> maybeWrapSmartPtr com ctx ent + + if ctx.SkipRecordTypeRegistration || isFableLibrary com || not ent.IsFSharpRecord then + expr + else + let registrationCtx = { ctx with SkipRecordTypeRegistration = true } + let typ = Fable.DeclaredType(entRef, genArgs) + let registration = makeRecordTypeInfo com registrationCtx r entRef genArgs typ + [ registration |> mkSemiStmt; expr |> mkExprStmt ] |> mkStmtBlockExpr let tryUseKnownUnionCaseNames fullName = match fullName with @@ -2202,7 +2271,7 @@ module Util = |> List.map (fun ident -> let oIdent = makeTypedIdent objType "o" let recExpr = Fable.TypeCast(Fable.IdentExpr oIdent, typ) // unbox obj -> Foo - let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type) + let fieldInfo = Fable.FieldInfo.Create(ident.Name, ident.Type, ident.IsMutable) let fieldGet = Fable.Get(recExpr, fieldInfo, ident.Type, None) let body = Fable.TypeCast(fieldGet, objType) // box the field value Fable.Delegate([ oIdent ], body, None, Fable.Tags.empty) @@ -2339,6 +2408,19 @@ module Util = let ctx = { ctx with IsParamByRefPreferred = false } com.TransformExpr(ctx, e) + let expr = + if isThisArgumentIdentExpr ctx e && not targetIsRef && not ctx.IsFluentMember then + let expr = expr |> makeClone + + match shouldBeRefCountWrapped com ctx e.Type with + | Some Lrc -> makeLrcPtrValue com ctx expr + | Some Rc -> makeRcValue com ctx expr + | Some Arc -> makeArcValue com ctx expr + | Some Box -> makeBoxValue com ctx expr + | None -> expr + else + expr + match sourceIsRef, targetIsRef, implCopy, implClone, mustClone, isUnreachable with | true, true, _, _, _, false -> expr | false, true, _, _, _, false -> expr |> mkAddrOfExpr @@ -2383,7 +2465,6 @@ module Util = assert (ent.IsInterface) FSharp2Fable.Util.getInterfaceMembers com ent - |> Seq.filter (fun (ifc, memb) -> not memb.IsProperty) |> Seq.distinctBy (fun (ifc, memb) -> memb.CompiledName) // skip inherited overwrites let getInterfaceMemberNames (com: IRustCompiler) (entRef: Fable.EntityRef) = @@ -2394,7 +2475,11 @@ module Util = |> Set.ofSeq let makeObjectExprMemberDecl (com: IRustCompiler) ctx (memb: Fable.ObjectExprMember) = - let capturedIdents = getCapturedIdents com ctx (Some memb.Name) memb.Args memb.Body + let capturedIdents = + getCapturedIdents com ctx (Some memb.Name) memb.Args memb.Body + |> Map.toList + |> List.map (fun (_, (ident: Fable.Ident)) -> ident.Name, ident) + |> Map.ofList let thisArg = { makeIdent selfName with IsThisArgument = true } |> Fable.IdentExpr @@ -2465,17 +2550,6 @@ module Util = memberDecl ) - let decl: Fable.ClassDecl = - { - Name = entName - Entity = entRef - Constructor = None - BaseCall = baseCall - AttachedMembers = members - XmlDoc = None - Tags = [] - } - let fieldIdents = fieldsMap.Values // |> Seq.map (fun ident -> { ident with Type = FableTransforms.uncurryType ident.Type }) @@ -2485,6 +2559,18 @@ module Util = fieldIdents |> List.map (fun ident -> let expr = transformIdent com ctx None ident |> makeClone + + let expr = + if ident.IsThisArgument then + match shouldBeRefCountWrapped com ctx ident.Type with + | Some Lrc -> makeLrcPtrValue com ctx expr + | Some Rc -> makeRcValue com ctx expr + | Some Arc -> makeArcValue com ctx expr + | Some Box -> makeBoxValue com ctx expr + | None -> expr + else + expr + let fieldName = ident.Name |> sanitizeMember mkExprField [] fieldName expr false false ) @@ -2497,6 +2583,122 @@ module Util = else fieldIdents + let interfaceGenMap = + let genericNames = + ent.GenericParameters |> List.map (fun parameter -> parameter.Name) + + List.zip genericNames genArgs |> Map + + let existingMemberNames = + members + |> List.map (getDeclMember com >> fun memberValue -> memberValue.CompiledName) + |> Set.ofList + + let eventPropertyNames = + existingMemberNames + |> Seq.choose (fun memberName -> + if memberName.StartsWith("add_", StringComparison.Ordinal) then + Some(memberName.Substring(4)) + elif memberName.StartsWith("remove_", StringComparison.Ordinal) then + Some(memberName.Substring(7)) + else + None + ) + |> Set.ofSeq + + let eventGetters = + FSharp2Fable.Util.getInterfaceMembers com ent + |> Seq.filter (fun (_, interfaceMember) -> + interfaceMember.CompiledName.StartsWith("get_", StringComparison.Ordinal) + ) + |> Seq.distinctBy (fun (_, interfaceMember) -> interfaceMember.CompiledName) + |> Seq.choose (fun (_, interfaceMember) -> + let propertyName = interfaceMember.CompiledName.Substring(4) + + if + Set.contains interfaceMember.CompiledName existingMemberNames + || not (Set.contains propertyName eventPropertyNames) + then + None + else + fieldIdents + |> List.tryFind (fun ident -> + ident.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase) + ) + |> Option.map (fun backingField -> + let propertyType = + interfaceMember.ReturnParameter.Type + |> resolveInlineType interfaceGenMap + |> FableTransforms.uncurryType + + let interfaceType = Fable.DeclaredType(entRef, genArgs) + + let thisArg = { makeTypedIdent interfaceType "this" with IsThisArgument = true } + + let objectSelf = + { makeIdent selfName with IsThisArgument = true } |> Fable.IdentExpr + + let eventExpr = + Fable.Get( + objectSelf, + Fable.FieldInfo.Create(backingField.Name, backingField.Type), + backingField.Type, + None + ) + + let body = + Fable.Get( + eventExpr, + Fable.FieldInfo.Create("Publish", propertyType), + propertyType, + None + ) + + let memberRef = + Fable.MemberRef( + interfaceMember.DeclaringEntity |> Option.defaultValue entRef, + { + IsInstance = true + CompiledName = interfaceMember.CompiledName + NonCurriedArgTypes = Some [] + AttributeFullNames = [] + } + ) + + let memberDecl: Fable.MemberDecl = + { + Name = interfaceMember.CompiledName + Args = [ thisArg ] + Body = body + MemberRef = memberRef + IsMangled = false + ImplementedSignatureRef = None + UsedNames = Set.empty + XmlDoc = None + Tags = [] + } + + memberDecl + ) + ) + |> Seq.toList + + let members = members @ eventGetters + + let decl: Fable.ClassDecl = + { + Name = entName + Entity = entRef + Constructor = None + BaseCall = baseCall + AttachedMembers = members + XmlDoc = None + Tags = [] + } + + let objectGenArgs = + FSharp2Fable.Util.getGenParamTypes (genArgs @ (fieldIdents |> List.map (fun ident -> ident.Type))) + let fields = fieldIdents |> List.map (fun ident -> @@ -2514,13 +2716,13 @@ module Util = exprField :: exprFields | _ -> exprFields - let attrs = [ mkAttr "derive" (makeDerivedFrom com ctx ent) ] - let generics = makeGenerics com ctx genArgs - let genParams = FSharp2Fable.Util.getGenParamTypes genArgs + let attrs = [ mkAttr "derive" [ rawIdent "Clone" ] ] + let generics = makeGenerics com ctx objectGenArgs + let genParams = FSharp2Fable.Util.getGenParamTypes objectGenArgs let structItems = [ mkStructItem attrs entName fields generics ] - let memberItems = transformClassMembers com ctx genArgs decl + let memberItems = transformClassMembers com ctx objectGenArgs decl (Some genArgs) let objExpr = // match baseCall with @@ -2746,6 +2948,12 @@ module Util = if needGenArgs && (mi.IsModuleMember || not mi.IsInstanceMember) then match typ with | Fable.Tuple _ -> transformGenArgs com ctx [ typ ] + // FSharpEvent`2 has two F# generic args but the Rust FSharpEvent_2 + // struct only takes the args type, so drop the delegate type + | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`2") (_, + [ _delegateType + argsType ]) -> + transformGenArgs com ctx [ argsType ] | _ -> transformGenArgs com ctx typ.Generics else None @@ -4708,6 +4916,15 @@ module Util = let args = decl.Args let body = decl.Body + let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType + + let isFluentMember = + match memb.DeclaringEntity, returnType with + | Some declaringEntity, Fable.DeclaredType(returnEntity, _) -> + declaringEntity.FullName = returnEntity.FullName + | _ -> false + + let ctx = { ctx with IsFluentMember = isFluentMember } let body = if memb.IsInstance && not (memb.IsConstructor) then @@ -5427,14 +5644,81 @@ module Util = let ifcEnt = com.GetEntity(ifcEntRef) FSharp2Fable.Util.getEntityGenArgs ifcEnt + let specializeParameter (genMap: Map) (parameter: Fable.Parameter) = + { new Fable.Parameter with + member _.Attributes = parameter.Attributes + member _.Name = parameter.Name + member _.Type = parameter.Type |> resolveInlineType genMap + member _.IsIn = parameter.IsIn + member _.IsOut = parameter.IsOut + member _.IsNamed = parameter.IsNamed + member _.IsOptional = parameter.IsOptional + member _.DefaultValue = parameter.DefaultValue + } + + let specializeInterfaceMember (genMap: Map) (memberValue: Fable.MemberFunctionOrValue) = + { new Fable.MemberFunctionOrValue with + member _.DisplayName = memberValue.DisplayName + member _.CompiledName = memberValue.CompiledName + member _.FullName = memberValue.FullName + member _.Attributes = memberValue.Attributes + member _.HasSpread = memberValue.HasSpread + member _.IsInline = memberValue.IsInline + member _.IsPublic = memberValue.IsPublic + member _.IsPrivate = memberValue.IsPrivate + member _.IsInternal = memberValue.IsInternal + member _.IsConstructor = memberValue.IsConstructor + member _.IsInstance = memberValue.IsInstance + member _.IsExtension = memberValue.IsExtension + member _.IsValue = memberValue.IsValue + member _.IsMutable = memberValue.IsMutable + member _.IsGetter = memberValue.IsGetter + member _.IsSetter = memberValue.IsSetter + member _.IsProperty = memberValue.IsProperty + + member _.IsOverrideOrExplicitInterfaceImplementation = + memberValue.IsOverrideOrExplicitInterfaceImplementation + + member _.IsDispatchSlot = memberValue.IsDispatchSlot + + member _.GenericParameters = + memberValue.GenericParameters + |> List.filter (fun parameter -> not (Map.containsKey parameter.Name genMap)) + + member _.CurriedParameterGroups = + memberValue.CurriedParameterGroups + |> List.map (List.map (specializeParameter genMap)) + + member _.ReturnParameter = specializeParameter genMap memberValue.ReturnParameter + member _.DeclaringEntity = memberValue.DeclaringEntity + member _.ApparentEnclosingEntity = memberValue.ApparentEnclosingEntity + member _.ImplementedAbstractSignatures = memberValue.ImplementedAbstractSignatures + member _.XmlDoc = memberValue.XmlDoc + } + let ignoredInterfaceNames = set [ Types.ienumerable; Types.ienumerator ] - let transformClassMembers (com: IRustCompiler) ctx genArgs (classDecl: Fable.ClassDecl) = + let transformClassMembers + (com: IRustCompiler) + ctx + genArgs + (classDecl: Fable.ClassDecl) + (objectExprInterfaceGenArgs: Fable.Type list option) + = let entRef = classDecl.Entity let ent = com.GetEntity(entRef) let isObjectExpr = classDecl.Name = "ObjectExpr" + let objectExprInterfaceGenMap = + match objectExprInterfaceGenArgs with + | Some interfaceGenArgs -> + let genericNames = + ent.GenericParameters |> List.map (fun parameter -> parameter.Name) + + List.zip genericNames interfaceGenArgs |> Map + | None -> Map.empty + let entName = if isObjectExpr then classDecl.Name @@ -5461,7 +5745,14 @@ module Util = let interfaceMembers, nonInterfaceMembers = classDecl.AttachedMembers - |> List.map (fun decl -> decl, getDeclMember com decl) + |> List.map (fun decl -> + let memberValue = getDeclMember com decl + + if isObjectExpr then + decl, specializeInterfaceMember objectExprInterfaceGenMap memberValue + else + decl, memberValue + ) |> List.partition (snd >> isInterfaceMember com) let nonInterfaceImpls = @@ -5490,7 +5781,7 @@ module Util = let baseTypeOpt = if isObjectExpr then - Some(Fable.DeclaredType(entRef, genArgs)) + Some(Fable.DeclaredType(entRef, objectExprInterfaceGenArgs |> Option.defaultValue genArgs)) else ent.BaseType |> Option.map (fun t -> Fable.DeclaredType(t.Entity, t.GenericArgs)) @@ -5506,7 +5797,7 @@ module Util = let hasToString = nonInterfaceMembers |> List.exists (fun (d, m) -> m.CompiledName = "ToString") - let hasDebug = not (List.isEmpty ent.GenericParameters) + let hasDebug = isObjectExpr || not (List.isEmpty ent.GenericParameters) makeDisplayTraitImpls com ctx entName genParams hasToString hasDebug let operatorTraitImpls = @@ -5548,7 +5839,7 @@ module Util = // different arity - so blindly reusing genArgs would emit e.g. // `IDisposable`. Look those up in the declared type's hierarchy. if ifcEntRef.FullName = entRef.FullName then - genArgs + objectExprInterfaceGenArgs |> Option.defaultValue genArgs else ifcEntRef |> findInterfaceGenArgs com ent else @@ -5593,7 +5884,7 @@ module Util = |> entityItemWithVis com ctx ent let genArgs = FSharp2Fable.Util.getEntityGenArgs ent - let memberItems = transformClassMembers com ctx genArgs decl + let memberItems = transformClassMembers com ctx genArgs decl None entityItem :: memberItems let getVis (com: IRustCompiler) ctx declaringEntity isInternal isPrivate = @@ -6003,7 +6294,9 @@ module Compiler = ScopedSymbols = Map.empty // HasMultipleUses = false InferAnyType = false + SkipRecordTypeRegistration = false IsAssocMember = false + IsFluentMember = false IsLambda = false IsParamByRefPreferred = false RequiresSendSync = false diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 87bdcf4cf8..448e3f2a60 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -3130,7 +3130,13 @@ let enumerables (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr let events (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | ".ctor", _ -> - Helper.LibCall(com, "Event", "default", t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) + let constructor = + if i.DeclaringEntityFullName.EndsWith("`2", StringComparison.Ordinal) then + "default2" + else + "default" + + Helper.LibCall(com, "Event", constructor, t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) |> Some | "get_Publish", Some callee -> getFieldWith r t callee "Publish" |> Some | meth, Some callee -> makeInstanceCall r t i callee meth args |> Some @@ -3333,6 +3339,9 @@ let uris | (ExprType String) :: _ -> Helper.LibCall(com, "Uri", "tryCreateWithKind", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | _ :: (ExprType String) :: _ -> + Helper.LibCall(com, "Uri", "tryCreateFromString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> Helper.LibCall(com, "Uri", "tryCreateFromUri", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3349,6 +3358,8 @@ let uris | "get_IsAbsoluteUri" | "get_Scheme" | "get_Host" + | "get_Port" + | "get_IsDefaultPort" | "get_AbsolutePath" | "get_AbsoluteUri" | "get_PathAndQuery" diff --git a/src/fable-library-rust/src/Event.rs b/src/fable-library-rust/src/Event.rs index 7f873bfce6..a197032210 100644 --- a/src/fable-library-rust/src/Event.rs +++ b/src/fable-library-rust/src/Event.rs @@ -11,7 +11,8 @@ pub mod Event_ { use crate::Choice_::Choice_2; use crate::Microsoft::FSharp::Control::{IDelegateEvent_1, IEvent_2}; use crate::Native_::{ - getZeroObj, referenceEquals, refCell, Any, Func0, Func1, Func2, LrcPtr, MutCell, Vec, + getZeroObj, interface_cast, referenceEquals, refCell, Any, Func0, Func1, Func2, Lrc, LrcPtr, + MutCell, Vec, }; use crate::Observable_::{mkDisposable, mkObserver, noCompleted, noError}; use crate::System::{IDisposable, IObservable_1, IObserver_1}; @@ -20,9 +21,7 @@ pub mod Event_ { pub type Handler = Func2, T, ()>; // ----- AnonymousEvent: the published IEvent ----- - // A concrete type so the Publish field can be used through IEvent_2 - // (AddHandler/RemoveHandler) *and* unsize-coerced to `dyn IObservable_1` - // (in F#/JS an IEvent is also an IObservable). + // A concrete implementation of IEvent_2 and IObservable_1. #[derive(Clone)] pub struct AnonymousEvent { add_handler: Func1, ()>, @@ -91,11 +90,43 @@ pub mod Event_ { } } + #[derive(Clone)] + struct EventObservable { + source: LrcPtr, T>>, + } + + impl core::fmt::Debug for EventObservable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for EventObservable { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl IObservable_1 for EventObservable { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + self.source.Subscribe(observer) + } + } + + pub fn asObservable( + source: LrcPtr, T>>, + ) -> LrcPtr> { + interface_cast!( + LrcPtr::new(EventObservable { source }), + Lrc>, + ) + } + // ----- FSharpEvent`1 ----- #[derive(Clone)] pub struct FSharpEvent_1 { handlers: LrcPtr>>>, - pub Publish: LrcPtr>, + pub Publish: LrcPtr, T>>, } impl core::fmt::Debug for FSharpEvent_1 { @@ -140,10 +171,10 @@ pub mod Event_ { } } }); - let publish = LrcPtr::new(AnonymousEvent { + let publish = interface_cast!(LrcPtr::new(AnonymousEvent { add_handler, remove_handler, - }); + }), Lrc, T>>,); LrcPtr::new(FSharpEvent_1 { handlers, Publish: publish, @@ -161,6 +192,57 @@ pub mod Event_ { }) } + // ----- FSharpEvent`2 ----- + // Sender-aware events use the same normalized Handler shape as one-generic events. + #[derive(Clone)] + pub struct FSharpEvent_2 { + handlers: LrcPtr>>>, + pub Publish: LrcPtr, T>>, + } + + impl core::fmt::Debug for FSharpEvent_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl core::fmt::Display for FSharpEvent_2 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", core::any::type_name::()) + } + } + + impl FSharpEvent_2 { + pub fn Trigger(&self, sender: LrcPtr, value: T) { + let hs = self.handlers.get().clone(); + for handler in hs.iter() { + handler(sender.clone(), value.clone()); + } + } + } + + pub fn default2() -> LrcPtr> { + let handlers: LrcPtr>>> = refCell(Vec::new()); + let add_handler = Func1::new({ + let handlers = handlers.clone(); + move |handler: Handler| handlers.get_mut().push(handler) + }); + let remove_handler = Func1::new({ + let handlers = handlers.clone(); + move |handler: Handler| { + let vec = handlers.get_mut(); + if let Some(pos) = vec.iter().position(|existing| referenceEquals(&**existing, &*handler)) { + vec.remove(pos); + } + } + }); + let publish = interface_cast!(LrcPtr::new(AnonymousEvent { + add_handler, + remove_handler, + }), Lrc, T>>,); + LrcPtr::new(FSharpEvent_2 { handlers, Publish: publish }) + } + // ----- module functions ----- pub fn add( @@ -275,14 +357,14 @@ pub mod Event_ { )> { let sp = splitter.clone(); let first = choose( - Func1::new(move |v: T| match &*sp(v) { + Func1::new(move |v: T| match sp(v).as_ref() { Choice_2::Choice1Of2(x) => Some(x.clone()), _ => None, }), source.clone(), ); let second = choose( - Func1::new(move |v: T| match &*splitter(v) { + Func1::new(move |v: T| match splitter(v).as_ref() { Choice_2::Choice2Of2(x) => Some(x.clone()), _ => None, }), diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index a6c2383003..c16859d49c 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -118,6 +118,20 @@ pub mod Native_ { } } + #[cfg(not(feature = "lrc_ptr"))] + impl NullableRef for Lrc { + #[inline] + fn null() -> Self { + static NULL: OnceInit> = OnceInit::new(); + NULL.get_or_init(|| Lrc::new(())).clone() + } + + #[inline] + fn is_null(&self) -> bool { + Lrc::ptr_eq(self, &Self::null()) + } + } + impl NullableRef for Option { #[inline] fn null() -> Self { @@ -314,11 +328,10 @@ pub mod Native_ { unsafe { core::mem::zeroed() } // will panic on Rc/Arc/Box } - // A valid placeholder value of type `LrcPtr`. Used to pre-declare - // reference-typed (`obj`) match bindings, as `getZero`/`mem::zeroed` panics on - // the `Rc` fat pointer and `null` is not available for the unsized `dyn Any`. + // A valid null value of type `LrcPtr`, used for `obj` match bindings + // and event sender arguments. pub fn getZeroObj() -> LrcPtr { - box_(()) + null::>() } pub fn defaultOf() -> T { @@ -686,6 +699,7 @@ pub mod Native_ { #[cfg(not(feature = "lrc_ptr"))] pub fn box_(x: T) -> LrcPtr { + crate::Reflection_::register_type_name::(); match (&x as &dyn Any).downcast_ref::>() { Some(o) => o.clone(), None => LrcPtr::new(x) as LrcPtr, @@ -694,6 +708,7 @@ pub mod Native_ { #[cfg(feature = "lrc_ptr")] pub fn box_(x: T) -> LrcPtr { + crate::Reflection_::register_type_name::(); match (&x as &dyn Any).downcast_ref::>() { Some(o) => o.clone(), None => LrcPtr::from(Lrc::new(x) as Lrc), @@ -712,6 +727,7 @@ pub mod Native_ { // site) match the pointee's runtime type id for value-based reflection. #[cfg(not(feature = "lrc_ptr"))] pub fn box_lrc(o: LrcPtr) -> LrcPtr { + crate::Reflection_::register_type_name::(); o } @@ -720,11 +736,24 @@ pub mod Native_ { // pointer identity and the pointee's runtime type id are both preserved. #[cfg(feature = "lrc_ptr")] pub fn box_lrc(o: LrcPtr) -> LrcPtr { + crate::Reflection_::register_type_name::(); let inner: Lrc = Lrc::clone(&*o); let coerced: Lrc = inner; LrcPtr::from(coerced) } + pub fn box_rc(o: Rc) -> LrcPtr { + box_((*o).clone()) + } + + pub fn box_arc(o: Arc) -> LrcPtr { + box_((*o).clone()) + } + + pub fn box_box(o: Box) -> LrcPtr { + box_((*o).clone()) + } + pub fn unbox_lrc(o: LrcPtr) -> LrcPtr { #[cfg(not(feature = "lrc_ptr"))] let inner: Lrc = o; @@ -746,6 +775,18 @@ pub mod Native_ { } } + pub fn unbox_rc(o: LrcPtr) -> Rc { + Rc::new(unbox(o)) + } + + pub fn unbox_arc(o: LrcPtr) -> Arc { + Arc::new(unbox(o)) + } + + pub fn unbox_box(o: LrcPtr) -> Box { + Box::new(unbox(o)) + } + pub fn ofObj(value: T) -> Option { if is_null(&value) { None:: diff --git a/src/fable-library-rust/src/Observable.rs b/src/fable-library-rust/src/Observable.rs index 826370874e..9c1db5718d 100644 --- a/src/fable-library-rust/src/Observable.rs +++ b/src/fable-library-rust/src/Observable.rs @@ -1,6 +1,7 @@ pub mod Observable_ { use crate::Choice_::Choice_2; - use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr}; + use crate::Microsoft::FSharp::Control::IEvent_2; + use crate::Native_::{interface_cast, refCell, Func0, Func1, Func2, Lrc, LrcPtr, RefCell}; use crate::System::{Exception, IDisposable, IObservable_1, IObserver_1}; // ----------------------------------------------------------- @@ -92,6 +93,22 @@ pub mod Observable_ { } } + pub trait ObservableSource: Clone + 'static { + fn subscribe_source(&self, observer: LrcPtr>) -> LrcPtr; + } + + impl + ?Sized + 'static, T: Clone + 'static> ObservableSource for LrcPtr { + fn subscribe_source(&self, observer: LrcPtr>) -> LrcPtr { + self.Subscribe(observer) + } + } + + impl IObservable_1 for dyn IEvent_2 { + fn Subscribe(&self, observer: LrcPtr>) -> LrcPtr { + IEvent_2::Subscribe(self, observer) + } + } + // ----------------------------------------------------------- // Constructors // ----------------------------------------------------------- @@ -146,46 +163,111 @@ pub mod Observable_ { Func0::new(move || observer.OnCompleted()) } + fn protect(action: F, on_error: Func1, ()>) { + crate::Exception_::try_catch( + || action(), + |error| { + on_error(error); + }, + ); + } + + struct StoppedObserver { + stopped: RefCell, + on_next: Func1, + on_error: Func1, ()>, + on_completed: Func0<()>, + } + + fn stoppedObserver( + observer: LrcPtr>, + ) -> StoppedObserver { + let stopped = refCell(false); + let on_next = { + let stopped = stopped.clone(); + let observer = observer.clone(); + Func1::new(move |value: T| { + if !*stopped.get() { + observer.OnNext(value); + } + }) + }; + let on_error = { + let stopped = stopped.clone(); + let observer = observer.clone(); + Func1::new(move |error: LrcPtr| { + if !*stopped.get() { + stopped.set(true); + observer.OnError(error); + } + }) + }; + let on_completed = { + let stopped = stopped.clone(); + Func0::new(move || { + if !*stopped.get() { + stopped.set(true); + observer.OnCompleted(); + } + }) + }; + StoppedObserver { + stopped, + on_next, + on_error, + on_completed, + } + } + // ----------------------------------------------------------- // Combinators // ----------------------------------------------------------- - pub fn add(callback: Func1, source: LrcPtr>) { - source.Subscribe(mkObserver(callback, noError(), noCompleted())); + pub fn add, T: Clone + 'static>(callback: Func1, source: S) { + source.subscribe_source(mkObserver(callback, noError(), noCompleted())); } - pub fn subscribe( + pub fn subscribe, T: Clone + 'static>( callback: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr { - source.Subscribe(mkObserver(callback, noError(), noCompleted())) + source.subscribe_source(mkObserver(callback, noError(), noCompleted())) } - pub fn choose( + pub fn choose, T: Clone + 'static, U: Clone + 'static>( chooser: Func1>, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let chooser = chooser.clone(); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( Func1::new(move |t: T| { - if let Some(u) = chooser(t) { - obs.OnNext(u); - } + protect( + || { + if let Some(u) = chooser(t) { + on_next(u); + } + }, + fwdError(errorObserver.clone()), + ); }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn filter( + pub fn filter, T: Clone + 'static>( predicate: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr> { choose( Func1::new(move |x: T| if predicate(x.clone()) { Some(x) } else { None }), @@ -193,55 +275,51 @@ pub mod Observable_ { ) } - pub fn map( + pub fn map, T: Clone + 'static, U: Clone + 'static>( mapping: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let mapping = mapping.clone(); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( - Func1::new(move |t: T| obs.OnNext(mapping(t))), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + Func1::new(move |t: T| { + protect(|| on_next(mapping(t)), fwdError(errorObserver.clone())); + }), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn merge( - source1: LrcPtr>, - source2: LrcPtr>, + pub fn merge, S2: ObservableSource, T: Clone + 'static>( + source1: S1, + source2: S2, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { - let stopped = refCell(false); + let guard = stoppedObserver(observer.clone()); + let stopped = guard.stopped.clone(); + let on_next = guard.on_next.clone(); + let on_error = guard.on_error.clone(); let completed1 = refCell(false); let completed2 = refCell(false); let h1 = { - let stopped = stopped.clone(); let completed1 = completed1.clone(); let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); let obs_c = observer.clone(); - let se = stopped.clone(); let sc = stopped.clone(); - source1.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), + source1.subscribe_source(mkObserver( + on_next.clone(), + on_error.clone(), Func0::new(move || { if !*sc.get() { completed1.set(true); @@ -255,26 +333,13 @@ pub mod Observable_ { }; let h2 = { - let stopped = stopped.clone(); let completed1 = completed1.clone(); let completed2 = completed2.clone(); - let obs_n = observer.clone(); - let obs_e = observer.clone(); let obs_c = observer.clone(); - let se = stopped.clone(); let sc = stopped.clone(); - source2.Subscribe(mkObserver( - Func1::new(move |v: T| { - if !*stopped.get() { - obs_n.OnNext(v); - } - }), - Func1::new(move |e: LrcPtr| { - if !*se.get() { - se.set(true); - obs_e.OnError(e); - } - }), + source2.subscribe_source(mkObserver( + on_next, + on_error, Func0::new(move || { if !*sc.get() { completed2.set(true); @@ -295,8 +360,8 @@ pub mod Observable_ { )) } - pub fn pairwise( - source: LrcPtr>, + pub fn pairwise, T: Clone + 'static>( + source: S, ) -> LrcPtr>> { mkObservable(Func1::new( move |observer: LrcPtr>>| -> LrcPtr { @@ -312,14 +377,14 @@ pub mod Observable_ { fwdError(observer.clone()), fwdCompleted(observer.clone()), ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn partition( + pub fn partition, T: Clone + 'static>( predicate: Func1, - source: LrcPtr>, + source: S, ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { let pred = predicate.clone(); let first = filter(predicate, source.clone()); @@ -327,44 +392,53 @@ pub mod Observable_ { LrcPtr::new((first, second)) } - pub fn scan( + pub fn scan, U: Clone + 'static, T: Clone + 'static>( collector: Func2, state: U, - source: LrcPtr>, + source: S, ) -> LrcPtr> { mkObservable(Func1::new( move |observer: LrcPtr>| -> LrcPtr { let collector = collector.clone(); let st = refCell(state.clone()); - let obs = observer.clone(); + let guard = stoppedObserver(observer.clone()); + let on_next = guard.on_next.clone(); + let errorObserver = observer.clone(); + let on_error = guard.on_error.clone(); + let on_completed = guard.on_completed.clone(); let inner = mkObserver( Func1::new(move |t: T| { - let u = collector(st.get().clone(), t); - st.set(u.clone()); - obs.OnNext(u); + protect( + || { + let u = collector(st.get().clone(), t); + st.set(u.clone()); + on_next(u); + }, + fwdError(errorObserver.clone()), + ); }), - fwdError(observer.clone()), - fwdCompleted(observer.clone()), + on_error, + on_completed, ); - source.Subscribe(inner) + source.subscribe_source(inner) }, )) } - pub fn split( + pub fn split, T: Clone + 'static, U1: Clone + 'static, U2: Clone + 'static>( splitter: Func1>>, - source: LrcPtr>, + source: S, ) -> LrcPtr<(LrcPtr>, LrcPtr>)> { let sp = splitter.clone(); let first = choose( - Func1::new(move |v: T| match &*sp(v) { + Func1::new(move |v: T| match sp(v).as_ref() { Choice_2::Choice1Of2(x) => Some(x.clone()), _ => None, }), source.clone(), ); let second = choose( - Func1::new(move |v: T| match &*splitter(v) { + Func1::new(move |v: T| match splitter(v).as_ref() { Choice_2::Choice2Of2(x) => Some(x.clone()), _ => None, }), diff --git a/src/fable-library-rust/src/Path.rs b/src/fable-library-rust/src/Path.rs index d40f9d547e..39d4129bea 100644 --- a/src/fable-library-rust/src/Path.rs +++ b/src/fable-library-rust/src/Path.rs @@ -1,5 +1,6 @@ #[cfg(not(feature = "no_std"))] pub mod Path_ { + use crate::Native_::NullableRef; use crate::String_::{fromString, string}; fn is_sep(c: char) -> bool { @@ -24,12 +25,24 @@ pub mod Path_ { // or an empty string when there is no separator. Trailing separators on // the directory portion are trimmed, matching .NET semantics. pub fn getDirectoryName(path: string) -> string { + if path.is_null() { + return ::null(); + } + let s = path.as_str(); + if s.is_empty() || (s.len() == 1 && is_sep(s.chars().next().unwrap())) { + return ::null(); + } + match s.rfind(is_sep) { Some(i) => { let dir = &s[..i]; let dir = dir.trim_end_matches(is_sep); - fromString(dir.to_string()) + if dir.is_empty() && !s.is_empty() { + fromString(s[..1].to_string()) + } else { + fromString(dir.to_string()) + } } None => string(""), } diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs index f10f2e517c..4cd3b281b7 100644 --- a/src/fable-library-rust/src/Quotation.fs +++ b/src/fable-library-rust/src/Quotation.fs @@ -201,9 +201,140 @@ let isFieldGet (e: FSharpExpr) = Some(inst, pi, ([]: FSharpExpr list)) | _ -> None +let private joinStrings (separator: string) (parts: string list) = + match parts with + | [] -> "" + | first :: rest -> List.fold (fun result part -> result + separator + part) first rest + +let private operatorSymbol (methodName: string) = + match methodName with + | "op_Addition" -> Some "+" + | "op_Subtraction" -> Some "-" + | "op_Multiply" -> Some "*" + | "op_Division" -> Some "/" + | "op_Modulus" -> Some "%" + | "op_Exponentiation" -> Some "**" + | "op_Equality" -> Some "=" + | "op_Inequality" -> Some "<>" + | "op_LessThan" -> Some "<" + | "op_LessThanOrEqual" -> Some "<=" + | "op_GreaterThan" -> Some ">" + | "op_GreaterThanOrEqual" -> Some ">=" + | "op_BooleanAnd" -> Some "&&" + | "op_BooleanOr" -> Some "||" + | "op_UnaryNegation" -> Some "-" + | "op_LogicalNot" -> Some "not" + | _ -> None + +let private valueToString (value: obj) (typ: string) = + match typ with + | "string" -> "\"" + unbox value + "\"" + | "unit" -> "()" + | "bool" -> + if unbox value then + "true" + else + "false" + | "char" -> sprintf "%O" (unbox value) + | "int8" -> sprintf "%O" (unbox value) + | "uint8" -> sprintf "%O" (unbox value) + | "int16" -> sprintf "%O" (unbox value) + | "uint16" -> sprintf "%O" (unbox value) + | "int32" -> sprintf "%O" (unbox value) + | "uint32" -> sprintf "%O" (unbox value) + | "int64" -> sprintf "%O" (unbox value) + | "uint64" -> sprintf "%O" (unbox value) + | "nativeint" -> sprintf "%O" (unbox value) + | "unativeint" -> sprintf "%O" (unbox value) + | "float32" -> sprintf "%O" (unbox value) + | "float64" -> sprintf "%O" (unbox value) + | _ -> "" + +let rec exprToString (e: FSharpExpr) : string = + match e with + | ExprValue(value, typ) -> valueToString value typ + | ExprVarExpr v -> v.Name + | ExprLambda(v, body) -> "fun " + v.Name + " -> " + exprToString body + | ExprApplication(func, arg) -> exprToString func + " " + exprToString arg + | ExprLet(v, value, body) -> "let " + v.Name + " = " + exprToString value + " in " + exprToString body + | ExprIfThenElse(guard, thenExpr, elseExpr) -> + "if " + + exprToString guard + + " then " + + exprToString thenExpr + + " else " + + exprToString elseExpr + | ExprCall(_, methodName, args, _) -> + let renderedArgs = args |> List.ofArray |> List.map exprToString + + match operatorSymbol methodName, renderedArgs with + | Some symbol, [ left; right ] -> "(" + left + " " + symbol + " " + right + ")" + | Some symbol, [ operand ] -> symbol + operand + | _ -> methodName + "(" + joinStrings ", " renderedArgs + ")" + | ExprSequential(first, second) -> exprToString first + "; " + exprToString second + | ExprNewTuple elements -> + "(" + + (elements |> List.ofArray |> List.map exprToString |> joinStrings ", ") + + ")" + | ExprTupleGet(inner, index) -> "Item" + string (index + 1) + "(" + exprToString inner + ")" + | ExprFieldGet(inner, fieldName) -> exprToString inner + "." + fieldName + | _ -> "" + +let substitute (e: FSharpExpr) (substitution: FSharpVar -> FSharpExpr option) : FSharpExpr = + let rec substituteExpr expression = + match expression with + | ExprValue _ -> expression + | ExprVarExpr variable -> + match substitution variable with + | Some replacement -> replacement + | None -> expression + | ExprLambda(variable, body) -> ExprLambda(variable, substituteExpr body) + | ExprApplication(func, arg) -> ExprApplication(substituteExpr func, substituteExpr arg) + | ExprLet(variable, value, body) -> ExprLet(variable, substituteExpr value, substituteExpr body) + | ExprIfThenElse(guard, thenExpr, elseExpr) -> + ExprIfThenElse(substituteExpr guard, substituteExpr thenExpr, substituteExpr elseExpr) + | ExprCall(instance, methodName, args, declaringType) -> + let substitutedArgs = ResizeArray() + + for argument in args do + substitutedArgs.Add(substituteExpr argument) + + ExprCall(substituteExpr instance, methodName, substitutedArgs.ToArray(), declaringType) + | ExprSequential(first, second) -> ExprSequential(substituteExpr first, substituteExpr second) + | ExprNewTuple elements -> + let substitutedElements = ResizeArray() + + for element in elements do + substitutedElements.Add(substituteExpr element) + + ExprNewTuple(substitutedElements.ToArray()) + | ExprNewUnion(typeName, tag, caseName, fields) -> + let substitutedFields = ResizeArray() + + for field in fields do + substitutedFields.Add(substituteExpr field) + + ExprNewUnion(typeName, tag, caseName, substitutedFields.ToArray()) + | ExprNewRecord(typeName, fieldNames, values) -> + let substitutedValues = ResizeArray() + + for value in values do + substitutedValues.Add(substituteExpr value) + + ExprNewRecord(typeName, fieldNames, substitutedValues.ToArray()) + | ExprNewList(head, tail) -> ExprNewList(substituteExpr head, substituteExpr tail) + | ExprTupleGet(inner, index) -> ExprTupleGet(substituteExpr inner, index) + | ExprUnionTag inner -> ExprUnionTag(substituteExpr inner) + | ExprUnionField(inner, fieldIndex) -> ExprUnionField(substituteExpr inner, fieldIndex) + | ExprFieldGet(inner, fieldName) -> ExprFieldGet(substituteExpr inner, fieldName) + | ExprFieldSet(inner, fieldName, value) -> ExprFieldSet(substituteExpr inner, fieldName, substituteExpr value) + | ExprVarSet(target, value) -> ExprVarSet(substituteExpr target, substituteExpr value) + + substituteExpr e + // --- Free variables --- -let getFreeVars (e: FSharpExpr) : FSharpVar list = +let getFreeVars (e: FSharpExpr) : FSharpVar seq = let seen = System.Collections.Generic.HashSet() let acc = ResizeArray() @@ -223,7 +354,9 @@ let getFreeVars (e: FSharpExpr) : FSharpVar list = walk bound g walk bound t walk bound el - | ExprCall(_, _, args, _) -> + | ExprCall(instance, _, args, _) -> + walk bound instance + for a in args do walk bound a | ExprSequential(f, s) -> @@ -232,12 +365,29 @@ let getFreeVars (e: FSharpExpr) : FSharpVar list = | ExprNewTuple els -> for el in els do walk bound el + | ExprNewUnion(_, _, _, fields) -> + for field in fields do + walk bound field + | ExprNewRecord(_, _, values) -> + for value in values do + walk bound value + | ExprNewList(head, tail) -> + walk bound head + walk bound tail | ExprTupleGet(inner, _) -> walk bound inner + | ExprUnionTag inner -> walk bound inner + | ExprUnionField(inner, _) -> walk bound inner | ExprFieldGet(inner, _) -> walk bound inner + | ExprFieldSet(inner, _, value) -> + walk bound inner + walk bound value + | ExprVarSet(target, value) -> + walk bound target + walk bound value | _ -> () walk Set.empty e - List.ofSeq acc + acc.ToArray() |> Seq.ofArray // --- Evaluation (structural cases + common operators; SQL translation deconstructs // rather than evaluates, so this covers the tested subset). --- @@ -285,15 +435,29 @@ let private applyOperator (method: string) (args: obj list) : obj = failwithf "Cannot evaluate method: %s" method let evaluate (e: FSharpExpr) : obj = - let rec eval (env: Map) (e: FSharpExpr) : obj = + let rec eval (env: Map ref) (e: FSharpExpr) : obj = + let evalArrayField (values: obj[]) (index: int) = + if index < 0 || index >= values.Length then + failwith "Quotation field index is out of range" + + values.[index] + match e with - | ExprValue(v, _) -> v - | ExprVarExpr v -> Map.find v.Name env - | ExprLambda(v, body) -> box (fun (arg: obj) -> eval (Map.add v.Name arg env) body) + | ExprValue(v, typ) -> + match typ with + | "unit" -> v + | "list" -> box ([||]: obj[]) + | _ -> v + | ExprVarExpr v -> Map.find v.Name env.Value + | ExprLambda(v, body) -> + let capturedEnv = env.Value + box (fun (arg: obj) -> eval (ref (Map.add v.Name arg capturedEnv)) body) | ExprApplication(f, a) -> let func = unbox obj> (eval env f) func (eval env a) - | ExprLet(v, value, body) -> eval (Map.add v.Name (eval env value) env) body + | ExprLet(v, value, body) -> + let value = eval env value + eval (ref (Map.add v.Name value env.Value)) body | ExprIfThenElse(g, t, el) -> if unbox (eval env g) then eval env t @@ -302,7 +466,33 @@ let evaluate (e: FSharpExpr) : obj = | ExprSequential(a, b) -> eval env a |> ignore eval env b - | ExprCall(_, method, args, _) -> applyOperator method [ for a in args -> eval env a ] + | ExprCall(instance, method, args, _) -> + let evaluatedArgs = [ for argument in args -> eval env argument ] + + match method with + | "get_IsSome" + | "get_IsNone" -> + let values = unbox (eval env instance) + let isSome = values.Length > 0 && unbox values.[0] = 1 + + box ( + if method = "get_IsSome" then + isSome + else + not isSome + ) + | "get_IsCons" + | "get_IsEmpty" -> + let values = unbox (eval env instance) + let isCons = values.Length > 0 + + box ( + if method = "get_IsCons" then + isCons + else + not isCons + ) + | _ -> applyOperator method evaluatedArgs // A tuple literal evaluates to a boxed obj[] of its evaluated elements, mirroring // PHP (which produces a plain array). TupleGet then indexes into that array; without // this arm `eval inner` on a tuple literal would fall through to failwith and there @@ -313,7 +503,83 @@ let evaluate (e: FSharpExpr) : obj = // operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the // arithmetic dereferences the borrow) so the indexing type-checks. let i = index + 0 - (unbox (eval env inner)).[i] - | _ -> failwith "Cannot evaluate expression" + evalArrayField (unbox (eval env inner)) i + | ExprNewUnion(_, tag, _, fields) -> + let values = ResizeArray() + values.Add(box tag) + + for field in fields do + values.Add(eval env field) + + box (values.ToArray()) + | ExprNewRecord(_, fieldNames, values) -> + let result = ResizeArray() + + for i = 0 to fieldNames.Length - 1 do + result.Add(box fieldNames.[i]) + result.Add(eval env values.[i]) + + box (result.ToArray()) + | ExprNewList(head, tail) -> + let result = ResizeArray() + result.Add(eval env head) + + for value in unbox (eval env tail) do + result.Add(value) + + box (result.ToArray()) + | ExprUnionTag inner -> + let values = unbox (eval env inner) + evalArrayField values 0 + | ExprUnionField(inner, fieldIndex) -> + let values = unbox (eval env inner) + evalArrayField values (fieldIndex + 1) + | ExprFieldGet(inner, fieldName) -> + let values = unbox (eval env inner) + + if fieldName = "Head" then + evalArrayField values 0 + elif fieldName = "Tail" then + box values.[1..] + elif fieldName = "Value" && values.Length > 1 && unbox values.[0] = 1 then + values.[1] + else + let mutable result = None + let mutable index = 0 + + while index + 1 < values.Length && result.IsNone do + if unbox values.[index] = fieldName then + result <- Some values.[index + 1] + + index <- index + 2 + + match result with + | Some value -> value + | None -> failwithf "Quotation field not found: %s" fieldName + | ExprFieldSet(target, fieldName, value) -> + let target = unbox (eval env target) + let value = eval env value + + let rec findFieldIndex index = + if index + 1 >= target.Length then + None + elif unbox target.[index] = fieldName then + Some index + else + findFieldIndex (index + 2) + + match findFieldIndex 0 with + | Some index -> target.[index + 1] <- value + | None -> failwithf "Quotation field not found: %s" fieldName + + box 0 + | ExprVarSet(target, value) -> + let value = eval env value + + match target with + | ExprVarExpr variable -> env.Value <- Map.add variable.Name value env.Value + | _ -> failwith "Quotation VarSet target must be a variable" + + box 0 - eval Map.empty e + eval (ref Map.empty) e diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 713b62ecc2..3a95cf5ce6 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -4,12 +4,14 @@ pub mod Reflection_ { use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; use crate::Native_::{box_, Any, Func1, LrcPtr, Vec}; use crate::NativeArray_::{array_from, Array}; - use crate::String_::string; + use crate::String_::{fromString, string}; - #[cfg(not(feature = "no_std"))] + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] use core::cell::RefCell; #[cfg(not(feature = "no_std"))] use std::collections::HashMap; + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + use std::sync::{OnceLock, RwLock}; pub fn name() -> string { // TODO: map some common type names to .NET type names @@ -19,8 +21,105 @@ pub mod Reflection_ { // The object (System.Object / obj) representation on the Rust target. type obj = LrcPtr; + #[cfg(not(feature = "lrc_ptr"))] + fn object_type_id(value: &obj) -> TypeId { + (&**value).type_id() + } + + #[cfg(feature = "lrc_ptr")] + fn object_type_id(value: &obj) -> TypeId { + (***value).type_id() + } + + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + thread_local! { + static TYPE_NAME_REGISTRY: RefCell> = + RefCell::new(HashMap::new()); + } + + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + static TYPE_NAME_REGISTRY: OnceLock>> = OnceLock::new(); + + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + fn register_type_name_value(tid: TypeId, name: string) { + TYPE_NAME_REGISTRY.with(|r| { + r.borrow_mut().insert(tid, name); + }); + } + + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + fn register_type_name_value(tid: TypeId, name: string) { + TYPE_NAME_REGISTRY + .get_or_init(|| RwLock::new(HashMap::new())) + .write() + .unwrap() + .insert(tid, name); + } + + #[cfg(feature = "no_std")] + fn register_type_name_value(_tid: TypeId, _name: string) {} + + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + fn registered_type_name(tid: &TypeId) -> Option { + TYPE_NAME_REGISTRY.with(|r| r.borrow().get(tid).cloned()) + } + + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + fn registered_type_name(tid: &TypeId) -> Option { + TYPE_NAME_REGISTRY + .get_or_init(|| RwLock::new(HashMap::new())) + .read() + .unwrap() + .get(tid) + .cloned() + } + + #[cfg(feature = "no_std")] + fn registered_type_name(_tid: &TypeId) -> Option { + None + } + + #[cfg(not(feature = "no_std"))] + fn canonical_type_name() -> string { + let rust_name = core::any::type_name::(); + let name = if TypeId::of::() == TypeId::of::() { + "System.String" + } else { + match rust_name { + "bool" => "System.Boolean", + "char" => "System.Char", + "i8" => "System.SByte", + "u8" => "System.Byte", + "i16" => "System.Int16", + "u16" => "System.UInt16", + "i32" => "System.Int32", + "u32" => "System.UInt32", + "i64" => "System.Int64", + "u64" => "System.UInt64", + "f32" => "System.Single", + "f64" => "System.Double", + _ => rust_name, + } + }; + fromString(name.to_string()) + } + + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + pub fn register_type_name() { + register_type_name_value(TypeId::of::(), canonical_type_name::()); + } + + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + pub fn register_type_name() { + register_type_name_value(TypeId::of::(), canonical_type_name::()); + } + + #[cfg(feature = "no_std")] + pub fn register_type_name() {} + // Compile-time helper emitted by `typeof` to obtain a concrete type id. pub fn type_id() -> TypeId { + register_type_name::(); TypeId::of::() } @@ -46,7 +145,7 @@ pub mod Reflection_ { } // Registry mapping a record's *concrete* TypeId to its reflection info. - // Populated when `typeof` is evaluated. This is what makes + // Populated by `typeof` and generated record construction. This is what makes // value-based reflection (GetRecordFields(record)) possible: from a bare // boxed record we can recover its runtime TypeId and look the info up. // @@ -54,29 +153,51 @@ pub mod Reflection_ { // the value-first entry points degrade (like exception catching does). // Type-first reflection (typeof, MakeRecord, GetRecordElements) is // unaffected, as those carry the info in the `System.Type` value itself. - #[cfg(not(feature = "no_std"))] + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] thread_local! { static RECORD_REGISTRY: RefCell> = RefCell::new(HashMap::new()); } - #[cfg(not(feature = "no_std"))] + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + static RECORD_REGISTRY: OnceLock>> = OnceLock::new(); + + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] fn registry_insert(tid: TypeId, info: RecordTypeInfo) { RECORD_REGISTRY.with(|r| { r.borrow_mut().insert(tid, info); }); } + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + fn registry_insert(tid: TypeId, info: RecordTypeInfo) { + RECORD_REGISTRY + .get_or_init(|| RwLock::new(HashMap::new())) + .write() + .unwrap() + .insert(tid, info); + } + #[cfg(feature = "no_std")] fn registry_insert(_tid: TypeId, _info: RecordTypeInfo) { // no registry when no_std } - #[cfg(not(feature = "no_std"))] + #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] fn registry_get(tid: &TypeId) -> Option { RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) } + #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + fn registry_get(tid: &TypeId) -> Option { + RECORD_REGISTRY + .get_or_init(|| RwLock::new(HashMap::new())) + .read() + .unwrap() + .get(tid) + .cloned() + } + #[cfg(feature = "no_std")] fn registry_get(_tid: &TypeId) -> Option { None // no registry when no_std @@ -105,6 +226,7 @@ pub mod Reflection_ { fields: array_from(fields_vec), make, }; + register_type_name_value(tid, info.name.clone()); registry_insert(tid, info.clone()); box_(info) } @@ -126,7 +248,7 @@ pub mod Reflection_ { // The PropertyInfo carrier holds only a name (a quotation knows nothing more), so the // getter is looked up here, mirroring JS/TS where getValue(pi, v) reads v[pi.Name]. fn getterOf(record: &obj, name: &string) -> Func1 { - let tid = (**record).type_id(); + let tid = object_type_id(record); let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); let field = info .fields @@ -152,7 +274,7 @@ pub mod Reflection_ { // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] pub fn getRecordFields(record: obj, _flags: Option) -> Array { - let tid = (*record).type_id(); + let tid = object_type_id(&record); let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); let vals: Vec = info .fields @@ -193,6 +315,8 @@ pub mod Reflection_ { info.name.clone() } else if let Some(s) = (*typ).downcast_ref::() { s.clone() + } else if let Some(tid) = (*typ).downcast_ref::() { + registered_type_name(tid).unwrap_or_else(|| crate::String_::string("")) } else { crate::String_::string("") } @@ -201,25 +325,26 @@ pub mod Reflection_ { // obj.GetType() for a statically-erased value (static type = obj/Any). // Reads the boxed value's *runtime* type id and returns the concrete type's // registered reflection info, so `(box record : obj).GetType()` resolves to - // the real record type (not typeof) whenever that type was registered - // via a prior `typeof`. Falls back to returning the object itself as an - // opaque placeholder for unregistered types. + // the real record type (not typeof). Unregistered values receive a + // TypeId carrier, which still supports type identity and primitive names. pub fn getTypeFromObj(o: obj) -> obj { - let tid = (*o).type_id(); + let tid = object_type_id(&o); match registry_get(&tid) { Some(info) => box_(info), - None => o, + None => box_(tid), } } // System.Type equality (typeof = typeof). Compares the carried concrete - // TypeId when both operands are record type infos; otherwise falls back to - // comparing the boxed values' runtime type ids. + // TypeId when both operands are record type infos; otherwise compares the + // TypeId carried by each non-record type value. pub fn typeEquals(a: obj, b: obj) -> bool { let ta = (*a).downcast_ref::(); let tb = (*b).downcast_ref::(); match (ta, tb) { (Some(x), Some(y)) => x.tid == y.tid, + (Some(x), None) => (*b).downcast_ref::().map_or(false, |y| x.tid == *y), + (None, Some(y)) => (*a).downcast_ref::().map_or(false, |x| *x == y.tid), (None, None) => { // Non-record `System.Type` values carry a boxed concrete `TypeId`. // Downcast both and compare the *carried* ids; comparing the boxed @@ -227,11 +352,9 @@ pub mod Reflection_ { // and therefore make every non-record type compare equal. match ((*a).downcast_ref::(), (*b).downcast_ref::()) { (Some(x), Some(y)) => x == y, - _ => (*a).type_id() == (*b).type_id(), + _ => object_type_id(&a) == object_type_id(&b), } } - // One record, one non-record: definitely different types. - _ => false, } } } diff --git a/src/fable-library-rust/src/TimeSpan.rs b/src/fable-library-rust/src/TimeSpan.rs index b387b13c61..238e211793 100644 --- a/src/fable-library-rust/src/TimeSpan.rs +++ b/src/fable-library-rust/src/TimeSpan.rs @@ -24,6 +24,11 @@ pub mod TimeSpan_ { pub const min_value: TimeSpan = TimeSpan { ticks: i64::MIN }; pub const max_value: TimeSpan = TimeSpan { ticks: i64::MAX }; + #[inline] + fn truncate_f64(value: f64) -> f64 { + value as i64 as f64 + } + impl core::fmt::Debug for TimeSpan { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "{}", self.toString(string(""))) @@ -231,27 +236,27 @@ pub mod TimeSpan_ { } pub fn hours(&self) -> i32 { - (self.totalHours() - self.totalDays().trunc() * 24.0) as i32 + (self.totalHours() - truncate_f64(self.totalDays()) * 24.0) as i32 } pub fn minutes(&self) -> i32 { - (self.totalMinutes() - self.totalHours().trunc() * 60.0) as i32 + (self.totalMinutes() - truncate_f64(self.totalHours()) * 60.0) as i32 } pub fn seconds(&self) -> i32 { - (self.totalSeconds() - self.totalMinutes().trunc() * 60.0) as i32 + (self.totalSeconds() - truncate_f64(self.totalMinutes()) * 60.0) as i32 } pub fn milliseconds(&self) -> i32 { - (self.totalMilliseconds() - self.totalSeconds().trunc() * 1000.0) as i32 + (self.totalMilliseconds() - truncate_f64(self.totalSeconds()) * 1000.0) as i32 } pub fn microseconds(&self) -> i32 { - (self.totalMicroseconds() - self.totalMilliseconds().trunc() * 1000.0) as i32 + (self.totalMicroseconds() - truncate_f64(self.totalMilliseconds()) * 1000.0) as i32 } pub fn nanoseconds(&self) -> i32 { - (self.totalNanoseconds() - self.totalMicroseconds().trunc() * 1000.0) as i32 + (self.totalNanoseconds() - truncate_f64(self.totalMicroseconds()) * 1000.0) as i32 } pub fn negate(&self) -> TimeSpan { diff --git a/src/fable-library-rust/src/Uri.rs b/src/fable-library-rust/src/Uri.rs index dddf187d05..069ae57beb 100644 --- a/src/fable-library-rust/src/Uri.rs +++ b/src/fable-library-rust/src/Uri.rs @@ -12,6 +12,7 @@ pub mod Uri_ { original: string, relative: string, // raw string for relative URIs schemePart: string, + userInfoPart: string, hostPart: string, port: i32, // -1 when no explicit port pathPart: string, @@ -96,13 +97,21 @@ pub mod Uri_ { port == -1 || (scheme == "http" && port == 80) || (scheme == "https" && port == 443) + || (scheme == "ftp" && port == 21) + || (scheme == "ws" && port == 80) + || (scheme == "wss" && port == 443) } - fn authority_str(scheme: &str, host: &str, port: i32) -> String { - if is_default_port(scheme, port) { + fn authority_str(scheme: &str, user_info: &str, host: &str, port: i32) -> String { + let host_port = if is_default_port(scheme, port) { host.to_string() } else { format!("{}:{}", host, port) + }; + if user_info.is_empty() { + host_port + } else { + format!("{}@{}", user_info, host_port) } } @@ -135,8 +144,8 @@ pub mod Uri_ { // parse scheme://host:port/path?query#fragment , or the authority-less // form scheme:path?query#fragment (mailto:, tel:, urn:, ...). - // Returns (scheme, hasAuthority, host, port, path, query, fragment). - fn parse_absolute(s: &str) -> (String, bool, String, i32, String, String, String) { + // Returns (scheme, hasAuthority, userInfo, host, port, path, query, fragment). + fn parse_absolute(s: &str) -> (String, bool, String, String, i32, String, String, String) { let idx = scheme_end(s).unwrap(); let scheme = s[..idx].to_ascii_lowercase(); let after = &s[idx + 1..]; // everything after "scheme:" @@ -148,16 +157,34 @@ pub mod Uri_ { let authority = &rest[..auth_end]; let remainder = &rest[auth_end..]; - let (host, port) = match authority.rfind(':') { - Some(ci) => { - let h = &authority[..ci]; - let p = &authority[ci + 1..]; - match p.parse::() { - Ok(n) => (h.to_ascii_lowercase(), n), - Err(_) => (authority.to_ascii_lowercase(), -1), + let (user_info, host_port) = match authority.rfind('@') { + Some(index) => (authority[..index].to_string(), &authority[index + 1..]), + None => (String::new(), authority), + }; + let (host, port) = if host_port.starts_with('[') { + match host_port.find(']') { + Some(end) => { + let host = host_port[..=end].to_ascii_lowercase(); + let port = host_port[end + 1..] + .strip_prefix(':') + .and_then(|value| value.parse::().ok()) + .unwrap_or(-1); + (host, port) + } + None => (host_port.to_ascii_lowercase(), -1), + } + } else { + match host_port.rfind(':') { + Some(ci) => { + let h = &host_port[..ci]; + let p = &host_port[ci + 1..]; + match p.parse::() { + Ok(n) => (h.to_ascii_lowercase(), n), + Err(_) => (host_port.to_ascii_lowercase(), -1), + } } + None => (host_port.to_ascii_lowercase(), -1), } - None => (authority.to_ascii_lowercase(), -1), }; let (before_frag, fragment) = match remainder.find('#') { @@ -169,7 +196,7 @@ pub mod Uri_ { None => (before_frag.to_string(), String::new()), }; let path = if path.is_empty() { "/".to_string() } else { path }; - (scheme, true, host, port, path, query, fragment) + (scheme, true, user_info, host, port, path, query, fragment) } None => { // authority-less: scheme:path?query#fragment (no host/port) @@ -181,19 +208,20 @@ pub mod Uri_ { Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), None => (before_frag.to_string(), String::new()), }; - (scheme, false, String::new(), -1, path, query, fragment) + (scheme, false, String::new(), String::new(), -1, path, query, fragment) } } } fn parse_abs_uri(orig: &str) -> Uri { - let (scheme, has_authority, host, port, path, query, fragment) = parse_absolute(orig); + let (scheme, has_authority, user_info, host, port, path, query, fragment) = parse_absolute(orig); Uri { isAbsolute: true, hasAuthority: has_authority, original: fromSlice(orig), relative: string(""), schemePart: fromString(scheme), + userInfoPart: fromString(user_info), hostPart: fromString(host), port, pathPart: fromString(path), @@ -209,6 +237,7 @@ pub mod Uri_ { original: fromSlice(orig), relative: fromSlice(orig), schemePart: string(""), + userInfoPart: string(""), hostPart: string(""), port: -1, pathPart: string(""), @@ -227,9 +256,10 @@ pub mod Uri_ { None => (rel, String::new()), }; let (rel_path, query) = match before_frag.find('?') { - Some(qi) => (before_frag[..qi].to_string(), before_frag[qi..].to_string()), - None => (before_frag.to_string(), String::new()), + Some(qi) => (before_frag[..qi].to_string(), Some(before_frag[qi..].to_string())), + None => (before_frag.to_string(), None), }; + let rel_path_is_empty = rel_path.is_empty(); let base_path = base.pathPart.as_str(); let merged_path = if rel_path.is_empty() { base_path.to_string() @@ -241,16 +271,32 @@ pub mod Uri_ { }; // RFC 3986 §5.2.4: collapse `.`/`..` segments in the merged path. let new_path = remove_dot_segments(&merged_path); + let query = match query { + Some(query) => query, + None if rel_path_is_empty => base.queryPart.as_str().to_string(), + None => String::new(), + }; + let fragment = match rel.find('#') { + Some(_) => fragment, + None if rel.is_empty() => base.fragmentPart.as_str().to_string(), + None => String::new(), + }; let scheme = base.schemePart.as_str(); + let user_info = base.userInfoPart.as_str(); let host = base.hostPart.as_str(); - let auth = authority_str(scheme, host, base.port); - let orig = format!("{}://{}{}{}{}", scheme, auth, new_path, query, fragment); + let orig = if base.hasAuthority { + let auth = authority_str(scheme, user_info, host, base.port); + format!("{}://{}{}{}{}", scheme, auth, new_path, query, fragment) + } else { + format!("{}:{}{}{}", scheme, new_path, query, fragment) + }; Uri { isAbsolute: true, hasAuthority: base.hasAuthority, original: fromString(orig), relative: string(""), schemePart: base.schemePart.clone(), + userInfoPart: base.userInfoPart.clone(), hostPart: base.hostPart.clone(), port: base.port, pathPart: fromString(new_path), @@ -303,7 +349,12 @@ pub mod Uri_ { fn build_absolute(&self) -> String { let scheme = self.schemePart.as_str(); if self.hasAuthority { - let auth = authority_str(scheme, self.hostPart.as_str(), self.port); + let auth = authority_str( + scheme, + self.userInfoPart.as_str(), + self.hostPart.as_str(), + self.port, + ); format!( "{}://{}{}{}{}", scheme, @@ -338,6 +389,27 @@ pub mod Uri_ { self.hostPart.clone() } + pub fn port(&self) -> i32 { + self.ensure_absolute(); + if self.port != -1 { + self.port + } else { + match self.schemePart.as_str() { + "http" => 80, + "https" => 443, + "ftp" => 21, + "ws" => 80, + "wss" => 443, + _ => -1, + } + } + } + + pub fn isDefaultPort(&self) -> bool { + self.ensure_absolute(); + is_default_port(self.schemePart.as_str(), self.port) + } + pub fn absolutePath(&self) -> string { self.ensure_absolute(); self.pathPart.clone() @@ -428,6 +500,18 @@ pub mod Uri_ { } } + pub fn tryCreateFromString( + baseUri: LrcPtr, + relativeUri: string, + res: &MutCell>, + ) -> bool { + if !baseUri.isAbsolute { + return false; + } + res.set(LrcPtr::new(resolve(&baseUri, relativeUri.as_str()))); + true + } + pub fn tryCreateFromUri( baseUri: LrcPtr, relativeUri: LrcPtr, diff --git a/tests/Rust/Cargo.toml b/tests/Rust/Cargo.toml index 3487151d1d..ee81e8cb6a 100644 --- a/tests/Rust/Cargo.toml +++ b/tests/Rust/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [features] +lrc_ptr = ["fable_library_rust/lrc_ptr"] no_std = ["fable_library_rust/no_std"] static_do_bindings = ["fable_library_rust/static_do_bindings"] threaded = ["fable_library_rust/threaded"] diff --git a/tests/Rust/tests/src/EventTests.fs b/tests/Rust/tests/src/EventTests.fs index dde1561419..c5ced0c106 100644 --- a/tests/Rust/tests/src/EventTests.fs +++ b/tests/Rust/tests/src/EventTests.fs @@ -2,44 +2,30 @@ module Fable.Tests.EventTests open Util.Testing -// NOTE (Rust): these class-hosted event types are disabled on Rust. They trigger -// with `event.Trigger(this, arg)`, i.e. they pass the enclosing instance (`this`) -// as an event value. The Rust backend cannot recover the reference-counted `this` -// from a `&self` method body (it emits a by-value struct clone of the wrong type), -// and `member Event = event.Publish` exposes the event as `IEvent<_>`, which the -// backend does not coerce to `IObservable<_>` for `.Add`/`.Subscribe`. The plain -// `Event<'T>` module-function tests below (the core of this port) are unaffected. -// These pass on the JS/TS/Python targets. -// type ClassWithCLIEvent() = -// let event = new Event<_>() -// [] -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// -// type ClassWithNonCLIEvent() = -// let event = new Event<_>() -// member _.Event = event.Publish -// member this.TestEvent(arg) = -// event.Trigger(this, arg) - -// NOTE (Rust): the two-generic Event<'Delegate,'Args> (FSharpEvent`2, with a -// sender-carrying 2-arg Trigger) and CLI events declared on an *interface* are -// not supported by the Rust runtime port yet. The `Microsoft.FSharp.Control` -// runtime only implements FSharpEvent`1 (single generic, 1-arg Trigger). These -// types and the two tests that use them (below) are therefore disabled on Rust; -// they pass on the JS/TS/Python targets. -// type InterfaceWithCLIEvent<'t> = -// [] -// abstract Event : IEvent,'t> -// -// type ClassWithInterfaceWithCLIEvent<'t>() = -// let event = new Event<_,_>() -// member this.TestEvent(arg) = -// event.Trigger(this, arg) -// interface InterfaceWithCLIEvent<'t> with -// [] -// member _.Event = event.Publish +type ClassWithCLIEvent() = + let event = new Event<_>() + [] + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type ClassWithNonCLIEvent() = + let event = new Event<_>() + member _.Event = event.Publish + member this.TestEvent(arg) = + event.Trigger(this, arg) + +type InterfaceWithCLIEvent<'t> = + [] + abstract Event : IEvent,'t> + +type ClassWithInterfaceWithCLIEvent<'t>() = + let event = new Event<_,_>() + member this.TestEvent(arg) = + event.Trigger(this, arg) + interface InterfaceWithCLIEvent<'t> with + [] + member _.Event = event.Publish module tests = @@ -197,6 +183,60 @@ module tests = source.Trigger 6 equal 6 result + [] + let ``IEvent handlers receive a null sender`` () = + let mutable senderWasNull = false + let source = Event<_>() + source.Publish.AddHandler(new Handler<_>(fun s _ -> senderWasNull <- obj.ReferenceEquals(s, null))) + |> ignore + + source.Trigger 6 + senderWasNull |> equal true + + [] + let ``Classes can trigger CLI events on interfaces`` () = + let mutable result = 0 + let mutable senderWasReceived = false + let classWithEvent = new ClassWithInterfaceWithCLIEvent<_>() + (classWithEvent :> InterfaceWithCLIEvent<_>).Event.AddHandler(fun s x -> + senderWasReceived <- not (obj.ReferenceEquals(s, null)) + result <- x + ) + classWithEvent.TestEvent(5) + equal 5 result + equal true senderWasReceived + + [] + let ``Generic interface expression can have CLI events`` () = + let mutable actualSender = "" + let mutable result = false + let event = Event<_,_>() + let ifaceWithEvent = + { new InterfaceWithCLIEvent<_> with + [] + member _.Event = event.Publish } + ifaceWithEvent.Event.AddHandler(fun sender arg -> + actualSender <- unbox sender + result <- arg) + let expectedSender = "SENDER" + let expectedResult = true + event.Trigger(expectedSender, expectedResult) + equal expectedSender actualSender + equal expectedResult result + + [] + let ``Two-generic event preserves its sender`` () = + let mutable actualSender = "" + let mutable actualValue = false + let source: Event, bool> = Event<_, _>() + source.Publish.AddHandler(fun sender value -> + actualSender <- unbox sender + actualValue <- value) + + source.Trigger("SENDER", true) + equal "SENDER" actualSender + equal true actualValue + [] let ``IEvent.RemoveHandler works`` () = let mutable result = 0 @@ -210,26 +250,23 @@ module tests = source.Trigger 6 equal 0 result - // NOTE (Rust): disabled — see the ClassWithCLIEvent / ClassWithNonCLIEvent - // note above (passing `this` as an event value + IEvent->IObservable coercion - // for `.Add`/`.Subscribe`). These pass on JS/TS/Python. - // [] - // let ``Classes can trigger CLI events`` () = - // let mutable result = 0 - // let classWithEvent = new ClassWithCLIEvent() - // classWithEvent.Event.Add(fun (sender, x) -> result <- x) - // classWithEvent.TestEvent(5) - // equal 5 result - // - // [] - // let ``Classes can trigger non-CLI events`` () = - // let mutable result = "" - // let classWithEvent = new ClassWithNonCLIEvent() - // let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) - // classWithEvent.TestEvent("Hello") - // disp.Dispose() - // classWithEvent.TestEvent("Bye") - // equal "Hello" result + [] + let ``Classes can trigger CLI events`` () = + let mutable result = 0 + let classWithEvent = new ClassWithCLIEvent() + classWithEvent.Event.Add(fun (sender, x) -> result <- x) + classWithEvent.TestEvent(5) + equal 5 result + + [] + let ``Classes can trigger non-CLI events`` () = + let mutable result = "" + let classWithEvent = new ClassWithNonCLIEvent() + let disp = classWithEvent.Event.Subscribe(fun (sender, arg) -> result <- arg) + classWithEvent.TestEvent("Hello") + disp.Dispose() + classWithEvent.TestEvent("Bye") + equal "Hello" result // NOTE (Rust): disabled — needs the two-generic Event<_,_> (FSharpEvent`2, // 2-arg Trigger with sender) and CLI events declared on interfaces, which the diff --git a/tests/Rust/tests/src/ObservableTests.fs b/tests/Rust/tests/src/ObservableTests.fs index 0ed72d768e..d45ff6320d 100644 --- a/tests/Rust/tests/src/ObservableTests.fs +++ b/tests/Rust/tests/src/ObservableTests.fs @@ -9,22 +9,24 @@ type MyObserver<'T>(f) = member x.OnError e = () member x.OnCompleted() = () -// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for two -// Rust-backend limitations (the runtime combinators themselves are unaffected): -// 1. The disposable is a named generic class, not an inline -// `{ new IDisposable with ... }` object expression: the Rust backend does not -// propagate the enclosing type's generic parameter into a generated -// object-expression struct (it emits an inner struct that references `T` -// without declaring it -> rustc E0401). -// 2. Subscriptions are keyed by an int id and removed by id, rather than +type ErrorObserver(onError: exn -> unit) = + interface IObserver with + member _.OnNext _ = () + member _.OnError error = onError error + member _.OnCompleted() = () + +type CountingObserver(onNext: unit -> unit, onError: exn -> unit, onCompleted: unit -> unit) = + interface IObserver with + member _.OnNext _ = onNext () + member _.OnError error = onError error + member _.OnCompleted() = onCompleted () + +// NOTE: This mirrors the JS/TS/Python reference `MyObservable`, adapted for one +// Rust-backend limitation (the runtime combinators themselves are unaffected): +// Subscriptions are keyed by an int id and removed by id, rather than // `listeners.Remove(w)`: interface trait objects (`IObserver<'T>`) do not // implement Rust `PartialEq`, so removal-by-value of an interface element is // not supported. Behaviour is identical to the reference. -type private Unsubscriber<'T>(listeners: ResizeArray>, id: int) = - interface IDisposable with - member _.Dispose() = - let idx = listeners.FindIndex(fun (i, _) -> i = id) - if idx >= 0 then listeners.RemoveAt(idx) type MyObservable<'T>() = let listeners = ResizeArray>() @@ -32,15 +34,41 @@ type MyObservable<'T>() = member x.Trigger v = for (_, lis) in listeners do lis.OnNext v + member x.TriggerError error = + for (_, lis) in listeners do + lis.OnError error + member x.TriggerCompleted() = + for (_, lis) in listeners do + lis.OnCompleted() interface IObservable<'T> with member x.Subscribe w = let id = nextId nextId <- nextId + 1 listeners.Add((id, w)) - new Unsubscriber<'T>(listeners, id) :> IDisposable + { new IDisposable with + member _.Dispose() = + let idx = listeners.FindIndex(fun (i, _) -> i = id) + if idx >= 0 then listeners.RemoveAt(idx) } module tests = + let assertMatchesDotNetAfterCallbackError (source: MyObservable) (observable: IObservable) = + let mutable next = 0 + let mutable errors = 0 + let mutable completed = 0 + observable.Subscribe(CountingObserver( + (fun () -> next <- next + 1), + (fun _ -> errors <- errors + 1), + (fun () -> completed <- completed + 1))) + |> ignore + source.Trigger 1 + source.Trigger 2 + source.TriggerError (Exception "source") + source.TriggerCompleted() + equal 0 next + equal 3 errors + equal 0 completed + [] let ``IObservable.Subscribe works`` () = let source = MyObservable() @@ -81,6 +109,15 @@ module tests = source.Trigger (Choice1Of2 2) source.Trigger (Choice2Of2 3) +#if !NO_STD_NO_EXCEPTIONS + [] + let ``Observable.choose matches .NET after callback error`` () = + let source = MyObservable() + let chosen: IObservable = + Observable.choose (fun _ -> (failwith "boom" : int option)) source + assertMatchesDotNetAfterCallbackError source chosen +#endif + [] let ``Observable.filter works`` () = let source = MyObservable() @@ -96,6 +133,40 @@ module tests = |> Observable.add (equal false) source.Trigger true +#if !NO_STD_NO_EXCEPTIONS + [] + let ``Observable.map matches .NET after callback error`` () = + let source = MyObservable() + let mapped: IObservable = + Observable.map (fun _ -> (failwith "boom" : int)) source + assertMatchesDotNetAfterCallbackError source mapped + + [] + let ``Observable.map forwards callback exceptions to OnError`` () = + let source = MyObservable() + let mutable errors = 0 + let mapped = Observable.map (fun _ -> failwith "boom") source + mapped.Subscribe(ErrorObserver(fun _ -> errors <- errors + 1)) |> ignore + source.Trigger 1 + equal 1 errors + + [] + let ``Observable.scan forwards collector exceptions to OnError`` () = + let source = MyObservable() + let mutable errors = 0 + let scanned = Observable.scan (fun _ _ -> failwith "boom") 0 source + scanned.Subscribe(ErrorObserver(fun _ -> errors <- errors + 1)) |> ignore + source.Trigger 1 + equal 1 errors + + [] + let ``Observable.scan matches .NET after collector error`` () = + let source = MyObservable() + let scanned: IObservable = + Observable.scan (fun _ _ -> (failwith "boom" : int)) 0 source + assertMatchesDotNetAfterCallbackError source scanned +#endif + [] let ``Observable.merge works`` () = let source1 = MyObservable() diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs index 353a081eba..984a684dd7 100644 --- a/tests/Rust/tests/src/QuotationTests.fs +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -196,6 +196,16 @@ let ``Evaluate nested lambda`` () = let r = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> unbox r |> equal 7 +[] +let ``Evaluate union quotation`` () = + let result = LeafExpressionConverter.EvaluateQuotation (<@ Some 42 @> :> Expr) + obj.ReferenceEquals(result, null) |> equal false + +[] +let ``Evaluate list quotation`` () = + let result = LeafExpressionConverter.EvaluateQuotation (<@ [ 1; 2; 3 ] @> :> Expr) + obj.ReferenceEquals(result, null) |> equal false + // --- NewUnionCase / NewRecord deconstruction --- type private MyRec = { X: int; Y: int } @@ -354,3 +364,44 @@ let ``Instance call inside a quotation has an instance`` () = match q with | Lambda(_, Call(Some _, _mi, _)) -> () | _ -> failwith "Expected Lambda with an instance Call body" + +[] +let ``GetFreeVars includes call instances`` () = + let q = <@ fun (s: string) -> s.Trim() @> + + match q with + | Lambda(_, body) -> + body.GetFreeVars() + |> Seq.map (fun v -> v.Name) + |> Seq.toList + |> equal [ "s" ] + | _ -> failwith "Expected Lambda" + +[] +let ``GetFreeVars visits nested union and list nodes`` () = + let q = <@ fun x -> [ Some x ] @> + + match q with + | Lambda(_, body) -> + body.GetFreeVars() + |> Seq.map (fun v -> v.Name) + |> Seq.toList + |> equal [ "x" ] + | _ -> failwith "Expected Lambda" + +[] +let ``Quotation ToString works`` () = + let text = (<@ 2 + 3 @>).ToString() + (text.Length > 0) |> equal true + +[] +let ``Quotation Substitute replaces variables`` () = + let quotation = (<@ fun x -> x + 1 @> :> Expr) + let replacement = (<@ 42 @> :> Expr) + let substituted = + quotation.Substitute(fun _ -> Some replacement) + + match substituted with + | Lambda(_, Call(_, _, [ Value(value, _); _ ])) -> + unbox value |> equal 42 + | _ -> failwith "Expected the lambda variable to be substituted" diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs index bfa7462820..206d3acc82 100644 --- a/tests/Rust/tests/src/RecordReflectionTests.fs +++ b/tests/Rust/tests/src/RecordReflectionTests.fs @@ -1,5 +1,5 @@ -// Value-based reflection needs the type registry, which is std-only -// (it uses thread_local), so these tests do not apply to no_std builds. +// Value-based reflection uses a std-only type registry (it uses thread_local), so +// these tests do not apply to no_std builds. [] module Fable.Tests.RecordReflectionTests @@ -13,6 +13,7 @@ open Microsoft.FSharp.Reflection type TestRecord = { String: string; Int: int } type OtherRecord = { Z: int } +type UnprimedRecord = { Value: int } type SampleUnion = | CaseA @@ -49,15 +50,19 @@ let ``FSharpType.GetRecordFields exposes PropertyInfo names`` () = [] let ``FSharpValue.GetRecordFields returns boxed field values`` () = - // Value-based reflection resolves the type via the runtime registry, which is - // populated when typeof is evaluated (as the JS record test also does first). - FSharpType.IsRecord(typeof) |> equal true let record = { String = "a"; Int = 1 } let values = FSharpValue.GetRecordFields(record) values.Length |> equal 2 unbox values.[0] |> equal "a" unbox values.[1] |> equal 1 +[] +let ``FSharpValue.GetRecordFields works without prior typeof`` () = + let record: UnprimedRecord = { Value = 7 } + let values = FSharpValue.GetRecordFields(record) + values.Length |> equal 1 + unbox values.[0] |> equal 7 + [] let ``FSharpValue.GetRecordField reads an individual field`` () = let record = { String = "a"; Int = 1 } @@ -104,7 +109,6 @@ let ``FSharp.Reflection: Record (end to end)`` () = [] let ``obj.GetType round-trips to the concrete record type`` () = let record = { String = "a"; Int = 1 } - FSharpType.IsRecord(typeof) |> equal true // ensure registration ((box record).GetType() = typeof) |> equal true let rebuilt = unbox (FSharpValue.MakeRecord((box record).GetType(), FSharpValue.GetRecordFields(record))) @@ -123,3 +127,10 @@ let ``Boxing a record to obj and unboxing preserves reference identity`` () = let o: obj = box r let r2 = unbox o System.Object.ReferenceEquals(r, r2) |> equal true + +[] +let ``obj.GetType resolves an unregistered primitive`` () = + let typeForEquality = (box 1).GetType() + let typeForName = (box 1).GetType() + (typeForEquality = typeof) |> equal true + typeForName.FullName |> equal "System.Int32" diff --git a/tests/Rust/tests/src/SystemIOTests.fs b/tests/Rust/tests/src/SystemIOTests.fs index 4fcfadb38c..4417a5ee45 100644 --- a/tests/Rust/tests/src/SystemIOTests.fs +++ b/tests/Rust/tests/src/SystemIOTests.fs @@ -29,6 +29,21 @@ let ``Path.GetDirectoryName works`` () = Path.GetDirectoryName("temp/test.txt") |> equal "temp" +[] +let ``Path.GetDirectoryName preserves the root`` () = + Path.GetDirectoryName("/foo") + |> equal "/" + +[] +let ``Path.GetDirectoryName returns null for the root itself`` () = + isNull (Path.GetDirectoryName "/") + |> equal true + +[] +let ``Path.GetDirectoryName returns null for a null path`` () = + isNull (Path.GetDirectoryName null) + |> equal true + [] let ``Path.GetExtension works`` () = Path.GetExtension("temp/test.txt") diff --git a/tests/Rust/tests/src/UriTests.fs b/tests/Rust/tests/src/UriTests.fs index f6ac81de32..b2c1f66550 100644 --- a/tests/Rust/tests/src/UriTests.fs +++ b/tests/Rust/tests/src/UriTests.fs @@ -64,6 +64,33 @@ let ``Uri from baseUri with relative string works`` () = equal "#c" uri.Fragment equal "http://www.test2.com/hello?a=b#c" uri.AbsoluteUri +[] +let ``Uri relative resolution preserves base query and fragment when omitted`` () = + let baseUri = Uri("http://host/a/b?old=1#base") + let same = Uri(baseUri, "") + equal "http://host/a/b?old=1#base" same.AbsoluteUri + + let fragmentOnly = Uri(baseUri, "#new") + equal "http://host/a/b?old=1#new" fragmentOnly.AbsoluteUri + + let queryOnly = Uri(baseUri, "?new=2") + equal "http://host/a/b?new=2" queryOnly.AbsoluteUri + +[] +let ``Uri host excludes user info and exposes ports`` () = + let uri = Uri("http://user:pass@host:8080/path") + equal "host" uri.Host + equal 8080 uri.Port + equal false uri.IsDefaultPort + + let defaultUri = Uri("http://host/path") + equal 80 defaultUri.Port + equal true defaultUri.IsDefaultPort + + let ftpUri = Uri("ftp://host/path") + equal 21 ftpUri.Port + equal true ftpUri.IsDefaultPort + [] let ``Uri from baseUri with relativeUri works`` () = let uri = Uri(Uri("http://www.test3.com/"), Uri("/hello?a=b#c", UriKind.Relative)) @@ -149,6 +176,12 @@ let ``TryCreate from absolute uri and absolute uri should work`` () = equal true valid equal absUri uri +[] +let ``TryCreate from absolute uri and relative string works`` () = + let (valid, uri) = Uri.TryCreate(Uri("https://example.com/base"), "child") + equal true valid + equal "https://example.com/child" uri.AbsoluteUri + [] let ``TryCreate from absolute uri and different absolute uri yields the second`` () = // .NET: when the second URI is absolute it wins and the base is ignored. From 6286d4d5bc4d1ce73ba345615a7ecb81b20afebb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:38:22 +0200 Subject: [PATCH 004/322] chore: Bump the all-pip group with 2 updates (#4913) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 +-- uv.lock | 80 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ab80c76ab..7e720e8d63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.3,<0.17", + "ruff>=0.16.4,<0.17", "maturin>=1.14.1,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.4", "attrs>=26.1.0", "pyright>=1.1.411", - "ty>=0.0.72", + "ty>=0.0.74", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 9938ac2746..9504d79325 100644 --- a/uv.lock +++ b/uv.lock @@ -68,8 +68,8 @@ dev = [ { name = "pydantic", specifier = ">=2.13.4" }, { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.3,<0.17" }, - { name = "ty", specifier = ">=0.0.72" }, + { name = "ruff", specifier = ">=0.16.4,<0.17" }, + { name = "ty", specifier = ">=0.0.74" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.3" +version = "0.16.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, - { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, - { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, - { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, - { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, - { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, - { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, - { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, - { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, + { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, + { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, + { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, + { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, + { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, ] [[package]] name = "ty" -version = "0.0.72" +version = "0.0.74" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/df/656e684bafb13c1d146e7d5b5f3e7978ca177232acc84998ff36427e9462/ty-0.0.72.tar.gz", hash = "sha256:ec2b8066b618df18cab4cb8e992f8da45d360332acb23fa34df7fa29cd1b9d3a", size = 6654939, upload-time = "2026-08-14T21:35:42.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/0f/c767853e88567a2ec7e996dd95e3105b1bc62c95d103689311ef0f4a603c/ty-0.0.74.tar.gz", hash = "sha256:da14344fc8625fc9ff359bafb856ad575636ea86d9bb6a629b146bff27b380e6", size = 6786318, upload-time = "2026-08-22T15:05:54.054Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/3b/f51461239a4e66565d4b362f97a3b55fe7fdba2e944068341f87c62f6743/ty-0.0.72-py3-none-linux_armv6l.whl", hash = "sha256:fda86db153ffd85ee52000cf175d6a3f1c0223772cf7c5b6f726200bf92c7b44", size = 12621989, upload-time = "2026-08-14T21:35:01.676Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/79ddf683affc679ca856f3510b5640ec3a88a842ba5f654f5d4bc78f1786/ty-0.0.72-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ceb944c612529b9023acfdc9cf4c0dcbb722549f9d17d46baecd1141baf01d7f", size = 12233910, upload-time = "2026-08-14T21:35:04.334Z" }, - { url = "https://files.pythonhosted.org/packages/5d/45/10562a0d84802158db8fa4ec46de54aa9fdcecdeeaabbfe3639ae7042b66/ty-0.0.72-py3-none-macosx_11_0_arm64.whl", hash = "sha256:108d76218333d6c092e5f1cebf8e9b06f25738613a0236a28e2dd47c936ee52c", size = 12084108, upload-time = "2026-08-14T21:35:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/a1/dc/1fe1aef8d697e3509face271a5331700c7aa1d1e44a4b622707bdfa41d4b/ty-0.0.72-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f3943f186f741a2499a31053872169250c9264a9a49684920e48d8fcf4ef4f5", size = 12132640, upload-time = "2026-08-14T21:35:09.305Z" }, - { url = "https://files.pythonhosted.org/packages/14/46/41ceb265e96969487311a2014bd0e53abb4fbc1395efb2ebe411fcb4db62/ty-0.0.72-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf283c07dc3cc52ca48a3ad8ab100fb5aec3aebbd03ef6a12d5f910b8e596fc5", size = 12402489, upload-time = "2026-08-14T21:35:11.555Z" }, - { url = "https://files.pythonhosted.org/packages/2b/45/30bf43cb4fd505c5c2dd30fda27dde5f05208686cd21217adec77c954204/ty-0.0.72-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f3b6462c38f9f115d10cee21f47fedf715fcf2040daf36eef210359300bc7c", size = 13130835, upload-time = "2026-08-14T21:35:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/31/2f/03bba754d2613f640df168335c41f83f41db150bb515839c60d80e3a7880/ty-0.0.72-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30caf658feb8ffb250d9e9e47107657a78f5f3425c227df1664d8df2ebe38880", size = 13590392, upload-time = "2026-08-14T21:35:16.839Z" }, - { url = "https://files.pythonhosted.org/packages/04/c7/03c67f00e63005ec41585653dc3096064570b1e6273742baae2798cd242f/ty-0.0.72-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27bdc012ddfbeec8948e4a6036c0dc39ac7cf2c8ec7c7d48dc7d2fd56d57b399", size = 13309629, upload-time = "2026-08-14T21:35:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/c1/df/102d3b264eb7f2a58dd11952f229bb5150bb5668d176a6154976a6675981/ty-0.0.72-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:802c5970a77d7739e6f499921fbb6984fb7ad8a31d95e1ff42fd46f3642e4f3b", size = 12734028, upload-time = "2026-08-14T21:35:22.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/85/d0737c8c54d0ba67366ddfb9f31d88edf0b02299e65923e6945ae60ebcb5/ty-0.0.72-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:47dce65114fdc615c68ca0edb393b433df0956447e4267df0e264137a789598d", size = 13174832, upload-time = "2026-08-14T21:35:24.71Z" }, - { url = "https://files.pythonhosted.org/packages/1e/31/497f5a96c36d9b586ab6afe0574986835c6fd5b835a89773d2bec4711b49/ty-0.0.72-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:325144fa07e2675d0faa337fcc864213c272a499eb0cfe5bde2fdc62282d27bc", size = 12215005, upload-time = "2026-08-14T21:35:26.892Z" }, - { url = "https://files.pythonhosted.org/packages/df/7d/46e65b17b4966c7cd0140f134380d33d8e84fe6efccd761533ce793dc502/ty-0.0.72-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5c9f15d0f58e43707d8848274be1821a0ef408eccb8aa7dda28a4a9eddf7640", size = 12421298, upload-time = "2026-08-14T21:35:29.301Z" }, - { url = "https://files.pythonhosted.org/packages/08/2a/12ada4ec17700b3cb1d4fd3bc3e5b1852df9e6885288429318cade87b3c1/ty-0.0.72-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee508d64b381871529cc22c412b41071bf5e908b7aa5d66a38f3f6b2573a806", size = 12669242, upload-time = "2026-08-14T21:35:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1a/4692536880790fb550ed6d44a6096778dc71bb112f2c6d615cebb01a57e5/ty-0.0.72-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3699e2ec7921d44da79d6b089f7bf239b2cc53c4e45a5a38430adc34ee9e9a55", size = 12988199, upload-time = "2026-08-14T21:35:33.749Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0d/f5e5a50322e9c45865e7b7a428ba6cd6527387cf0f2472492ac3cf746243/ty-0.0.72-py3-none-win32.whl", hash = "sha256:f25f72a67bd36cd247707c4784e52fad0b6b4f42a1b7dd14804110fa95c486ed", size = 11939708, upload-time = "2026-08-14T21:35:36.006Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4e/8af3534b2e4214e6184a5a59c34101e94a68d578f081f97b995866bab1bf/ty-0.0.72-py3-none-win_amd64.whl", hash = "sha256:cdeee869341717e1736cea2e2d7856738c6957c320f584ed2f68c8f90100d2f5", size = 12643876, upload-time = "2026-08-14T21:35:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/a2606e654c7276bd08586391a2525b0af3f3bf60228a8c57b2d248f273f9/ty-0.0.72-py3-none-win_arm64.whl", hash = "sha256:1bd3ac3ed4424a6d6990a85dc388556aea012bd752de21349a84b685951de0d8", size = 12394857, upload-time = "2026-08-14T21:35:40.277Z" }, + { url = "https://files.pythonhosted.org/packages/2c/95/6ded58bc97885c6d88fa1f9cd815031489200738f961cbf0466663213f80/ty-0.0.74-py3-none-linux_armv6l.whl", hash = "sha256:8969ef4e508debf00cf58f9ea85a539f799b1732c59cdfcecd037630b9755b30", size = 12790043, upload-time = "2026-08-22T15:05:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8a/5e323603b6ab8731144421877ee8a0f8ac5a5511e67857127caa09f6730e/ty-0.0.74-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51fb6cf5b98e1e1140825b2430943f78d744876a735231656eafbb4c3f7eca3c", size = 12371748, upload-time = "2026-08-22T15:05:08.609Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/ee72e08cb705281e8d8c42917dd577aa598a8a098008495fda5176ee3f6e/ty-0.0.74-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ebe60b1f0a948c793d6c77fc9e9ddda599e4f023c04ab16e8e03bcb428c3fa0", size = 12282403, upload-time = "2026-08-22T15:05:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/fd935b694ff68bc278af50f7ad04770b36ce6306399baef7e1847b553a9d/ty-0.0.74-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa97f407a695c890a53615966a663c7d2167e2cabe88db7ca1a24d62635cdfc8", size = 12345164, upload-time = "2026-08-22T15:05:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/5b5825268e029ebb164c909780103dbbae367f069801410068bf1cef29b3/ty-0.0.74-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:673ddb733d4a0db31385ba1ed9ff1f6bd9dc5565413ce57b1ca5ac4c7803da5d", size = 12556646, upload-time = "2026-08-22T15:05:16.994Z" }, + { url = "https://files.pythonhosted.org/packages/56/e7/515914e571d62ce0101744fed3f881936eeb1b30dc37beb72b4f7ca1e289/ty-0.0.74-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1028e7c6b4f6145e9704552f43a5fffdcd51b42263ffdcd9c9677762bc395a4a", size = 13311653, upload-time = "2026-08-22T15:05:20.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/d1452babb6f9266c2122cabc095180b70ed306fb770b2996753814d2237d/ty-0.0.74-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a8890493021fb308772474983316eb91f7b56cb227a6a05a06b262a36f0", size = 13768284, upload-time = "2026-08-22T15:05:23.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/8d4a2fc7842a47210a1cb0a16a187d9de39ad5d509a00fb74c1c073afcde/ty-0.0.74-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94859d321f3c6a6c8f7bfc3f40e8319cda7e6e012e613440f3dfd145d5010e2e", size = 13422306, upload-time = "2026-08-22T15:05:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/76/ebbc269a8c4efcc4d44624993bd188145f20d60ebda9680b15aaec42cc50/ty-0.0.74-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970a8b2c09ff3be04c8a1c6767332d861be4fce85efe7bb205e4ade7c8655274", size = 12970637, upload-time = "2026-08-22T15:05:29.15Z" }, + { url = "https://files.pythonhosted.org/packages/9e/dd/b99f7236acbf856780ca1779a48143d2d9f2c24d7f531a0ce15a022b8a87/ty-0.0.74-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:795f763b3ded85574c2c2846a6fb8acf2aa76e9e83d761143e92b1f0c7ffa2cd", size = 13344891, upload-time = "2026-08-22T15:05:32.033Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/9ff7449a4c7e6428f2c6f298e74cf24b70668f29d45c249507a723ff3782/ty-0.0.74-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc086db5367d912c31c0cc872deb7387290e779a4b9b54fcb944673a7cd52c7b", size = 12395272, upload-time = "2026-08-22T15:05:34.702Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/b23a5b6b35d37df89dc8dc5daa09efd9245a668b50c4c81c25de21567dc1/ty-0.0.74-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0314d7b391cf684e47c2fa093d2ce4c597cfc9b01d9a315fe204aed6359b271b", size = 12573079, upload-time = "2026-08-22T15:05:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/23/c5/ccba16239d6129533c8b3603458d0f4dd2ba69478e47059073968e74261d/ty-0.0.74-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4a45dd2e991e8bdae82ba78c8cd051b253f60bc71a6536598fa3ef580b4fc9b", size = 12832506, upload-time = "2026-08-22T15:05:40.505Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1c/2390912634dff4f341f97b397f2aee341ff062be0a66cda37d59375454f2/ty-0.0.74-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:210e2eac6b018fb934e2b8dac3956a0ba076a3fb1fa6f135058c825e5b759b81", size = 13154752, upload-time = "2026-08-22T15:05:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/a8c12188227e6f74f91853a7374e01ed81d6ad21c16c8b70e92dbebfe46a/ty-0.0.74-py3-none-win32.whl", hash = "sha256:db0bb6a8f098ef9bd1be861f73b4f7c0320d40d4c05c7ae0a8677d4e7aa4f6e5", size = 12130002, upload-time = "2026-08-22T15:05:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/21/5c/064f28ccb9c234cfce5a2f7aa69a256663d5ae5bb0290b3a9706cc4d1e4c/ty-0.0.74-py3-none-win_amd64.whl", hash = "sha256:bebff181515255b3c78bd2e7693ae66fab6064ad4feea2065c68bc01022aa678", size = 12771435, upload-time = "2026-08-22T15:05:48.811Z" }, + { url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" }, ] [[package]] From ef796704209a60e670c51709f9714024caccab27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:50:17 +0200 Subject: [PATCH 005/322] chore: Bump the all-nuget group with 2 updates (#4914) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6c453be0d1..cea3f2d9f0 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "7.0.5", + "version": "7.0.6", "commands": [ "fantomas" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fb33a570e5..af703934c1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,7 +32,7 @@ all analyzers - + all analyzers From be641d801b4c53dc9f98bff54d45140dd92615d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:50:55 +0200 Subject: [PATCH 006/322] chore: Bump hypothesis from 6.165.9 to 6.165.10 in /src/fable-library-py in the all-pip group (#4911) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 164 +++++++++++++++++------------------ 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index c74e4a75ee..50fd21f86a 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,94 +61,94 @@ dev = [ [[package]] name = "hypothesis" -version = "6.165.9" +version = "6.165.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/a8/f7afb4b55a7f00307f246fb7c3cf2abdc8f8e0ad7eb81bf23f8025480da6/hypothesis-6.165.9.tar.gz", hash = "sha256:a5d1fa312020b499c517e1217ef4440c56a9b52e20b47b420b48a583a81c412b", size = 503594, upload-time = "2026-08-15T02:50:07.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/c7/da4298709de649a01c73ce93c4ee5b5651433217e17c494dc91baded86b0/hypothesis-6.165.9-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:01a2a4106fa90396ffa1c402968a8bd25af056e1fdf7974a369c4057cc4035f1", size = 783107, upload-time = "2026-08-15T02:49:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/97/d9f265174464d05a4d818d98dcd4e870ad2c2b60e9844033c23b3eb21eda/hypothesis-6.165.9-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7812f1ce9e2bfc0958f2a96975f16a1e1f2400f9d8b7043723ff5811647fe0f6", size = 778677, upload-time = "2026-08-15T02:48:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fe/d3364f99dd76f1275b430b2fc2818119df84e71f0657ad7f89b922e90d55/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e547823eca16eab9c9569f292e5ce8f95d8a59f1d67e504e97a841d17eb0b562", size = 1107895, upload-time = "2026-08-15T02:47:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/42b524303219351add359a05463df067be126c9ca576b0547218cf8aa54b/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e50c687c0e86bb5898f36b845ab84439f44ef2369f0f9bbcbf6aa6860640324d", size = 1136455, upload-time = "2026-08-15T02:49:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/af/d5/91948f68b5007a40b2cc356d9365de0f5cb08c200e383b38259ad75130ae/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f82aed6f0631b5073bd87789aa56609e7c83bd762384fd24c84f3435816b2569", size = 1135112, upload-time = "2026-08-15T02:48:51.211Z" }, - { url = "https://files.pythonhosted.org/packages/55/3c/6e04ac80c5a63da9ecff00199851433437f617da9cd1b5208f11af3337b8/hypothesis-6.165.9-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488b88b6856e430763a7ac5a5fc6e20a798766dca8df8f94576de2281ee60afd", size = 1157397, upload-time = "2026-08-15T02:49:27.874Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/4a1aeddc254d8d6a8f43bbeea928ef5b576e041eb0c9459a91fce884f4cd/hypothesis-6.165.9-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:22dd9e264f2a79a1b63a5fae13351e3cf0088f584fb05bb0e23f21b8fba9df7c", size = 1112748, upload-time = "2026-08-15T02:48:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/35/bc/dde8231f1d10ab0c60627c40757b9b832605028ef0780420fcfc3ccba664/hypothesis-6.165.9-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:431ab894062666c93157234f9949a4db3549fbf3c220777abae3482e1b43f9bc", size = 1149484, upload-time = "2026-08-15T02:48:53.088Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b2/cb29a983c7f06770a6b348f76cb33f90a70b0594a1e61c4a803aedfb1d1f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c7edcc3dcec42b413c900f8e660f3741c21f0055c53bde21d728f72a21713247", size = 1283267, upload-time = "2026-08-15T02:47:55.688Z" }, - { url = "https://files.pythonhosted.org/packages/88/c0/6bfcf3fb379aba87f3d09073b589c7b7e932ad047ed7bc0124300a51f431/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0d4a613404ba216b6f698ce9df6f9c22d04d8e8a571c42525e83905cf1b1cd10", size = 1409841, upload-time = "2026-08-15T02:48:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/98/c0/0c73a985144717272d2afec377a45c149688a477af946ae24c16b59c9788/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:658c33d8a54cead6081f73fcc4e2c0fbe43d251d3f4170630748d8e5dcbd542f", size = 1264878, upload-time = "2026-08-15T02:50:05.056Z" }, - { url = "https://files.pythonhosted.org/packages/56/91/50d4a6df07a305f5f17578443c93b618a49e8db68c01de8870e539139eb9/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:7d3297fe95a253cea81b48e581e456105016ebe782d823dfb57ec09c3854301f", size = 1282519, upload-time = "2026-08-15T02:48:22.42Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5f/5a54f4ca30a669a247df7c467aea7bb854d9b4de7e1dc51edb2f91ec603f/hypothesis-6.165.9-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a9a1e17200a2b369a38c19c2184efbc041a40b784675ba72ac6800d8c1260a54", size = 1324669, upload-time = "2026-08-15T02:48:29.144Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b2/6ef8eac3b30d6c8225fc405be3b99790701a942706ec93a3bd638a0b614a/hypothesis-6.165.9-cp310-abi3-win32.whl", hash = "sha256:5d40a92c6d025725da7827431a71b7657ce2e3923ff8f1773d6b15d717f77ea0", size = 668903, upload-time = "2026-08-15T02:48:34.033Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/b9514f65d2cc97277f6442e3b05164177a742489940efa53e794a54b62f6/hypothesis-6.165.9-cp310-abi3-win_amd64.whl", hash = "sha256:59f29ef2965caf1ae367ee0598ac0d293551b95744a5d49888737a3b3d9dd944", size = 675051, upload-time = "2026-08-15T02:47:49.697Z" }, - { url = "https://files.pythonhosted.org/packages/96/01/110123106a3702d2f3ffc47b9bc6a1a29c19cd2d15fcf356430777a4883a/hypothesis-6.165.9-cp310-abi3-win_arm64.whl", hash = "sha256:f52fd4f1b95f614c3fec53840d603f0a3b19e68eaa91c8d609222999c3ee5506", size = 673402, upload-time = "2026-08-15T02:47:48.364Z" }, - { url = "https://files.pythonhosted.org/packages/09/c0/42f101a02ba2593c61ca4242b8a228d31599bfc2ab1bc5da3542fbdba9c2/hypothesis-6.165.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d7871212e7a1e9d317b2cc11934c6f28aaaa48b594231165a0963d3b3f5159b2", size = 783820, upload-time = "2026-08-15T02:49:19.948Z" }, - { url = "https://files.pythonhosted.org/packages/09/49/e49c94c9f09542e95785dbdaeb8aeac901571a26497dbeef1eedd8c95e20/hypothesis-6.165.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b80ffe65bed7f42681b845bdfde445c3da46a84eb2c4fdbc928521786b551c65", size = 779547, upload-time = "2026-08-15T02:48:06.225Z" }, - { url = "https://files.pythonhosted.org/packages/a3/74/173a59310db80f896059f96259c336c6a773c07d6298645a83ef8e1353fd/hypothesis-6.165.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7186be7b5ce1aee61614ede14fc6f3f072d94ab3c3549613903cd1c9a37848ea", size = 1108411, upload-time = "2026-08-15T02:49:04.88Z" }, - { url = "https://files.pythonhosted.org/packages/08/94/4f4091f4d8522fa6c9a7ee8ad71b6db30a7820faf18e5736318fac946569/hypothesis-6.165.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0a64a05255caa9a5ad90e1ab57eb26d64123b005234e22c80e2f723832e047", size = 1157885, upload-time = "2026-08-15T02:49:06.742Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4f/7b5afb5037df3f1c5e327824f3589ca94c4584cbeace0f6cae68f9148163/hypothesis-6.165.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4d7bbb89c256d02406b7875dbe4058ab79fba6d8e6b56dd1d771e0c9800a02d4", size = 1284010, upload-time = "2026-08-15T02:48:01.796Z" }, - { url = "https://files.pythonhosted.org/packages/7b/cf/8a98ab9d9476cf282054c399577ab7aba2984d2989dc907f20016b633e09/hypothesis-6.165.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:740074d1b6ae4a5335b7a17f44f776c3952d6c7f4a6c84f23ae93e1907834692", size = 1324995, upload-time = "2026-08-15T02:48:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/54/07/e0a0d86029f6bc709be2cf9bb77c7661d0e644fb631c9835116d96ce0e0c/hypothesis-6.165.9-cp310-cp310-win_amd64.whl", hash = "sha256:3295f35299c6d4f7bc12dc583bdca776a5e41cf22a185e5209800885950bb163", size = 674965, upload-time = "2026-08-15T02:48:14.04Z" }, - { url = "https://files.pythonhosted.org/packages/de/1d/1c2dea64a3b00b9f83ca57bacaf4982255da7797d6c65fc0301f21a0ca94/hypothesis-6.165.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a9995cbf91e0b22cb8d9aaaf6b10994b2ac1ebc3b01b4a91d3e82761a2737043", size = 783582, upload-time = "2026-08-15T02:49:29.881Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8c/d8f51166ef9feaeb08d33737b2874d1effca4ada8bf6598e6f2a634a3ca1/hypothesis-6.165.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f94e4456f7b0dd04f4f80f26a2c499370f6048f393ff61436de4e192e278e2df", size = 779367, upload-time = "2026-08-15T02:48:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/749aa12ec03fdcec1e4fa7289145f03ded46e892ae89613e333dd7090a7e/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bab95b4ef980cbc2b4632e49057588766cda930ffbd727d70edfea289da98dd3", size = 1108251, upload-time = "2026-08-15T02:47:59.983Z" }, - { url = "https://files.pythonhosted.org/packages/2b/cb/a1c417cd39f87ca11e2b5b4a7da0446ae316b43b9baee23e113a59113759/hypothesis-6.165.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8795161db0a7c270c4f3e0eb754bd0ed96b17133d6f851c7444f9733c4c9d37f", size = 1157689, upload-time = "2026-08-15T02:49:40.288Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d5/82abf4d3b171f087d323b179a422472e1c1621ed47adf88f0fcdf7be7e35/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c7dcb0a21dab077e65bcb082ee321b7e31402cf980da648b4369c71086ad53c", size = 1283592, upload-time = "2026-08-15T02:48:04.698Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ee/f156d374fd7f49220a789a231cd980ffe53bf1eec11a75306ded76265306/hypothesis-6.165.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92b264bef7860bf37b5f22b5949083d26f113351da8b99e93adb0554d3e2c110", size = 1324910, upload-time = "2026-08-15T02:48:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/23/57/d14969c84e59356103c0b5e1a27623a4149d5d371799757552b2ec344da2/hypothesis-6.165.9-cp311-cp311-win_amd64.whl", hash = "sha256:ae34d13d8438ea7f3ee18292290b9f608de6070309f3c5826644a88d197c319d", size = 674776, upload-time = "2026-08-15T02:48:59.008Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7e/03a84262b74300714af486e037403db8f5e19170f01bc32605300896b19d/hypothesis-6.165.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:41082aff3e438ac1eeb86002c269b9b5d86bab563e911614c2c8d34f405e02db", size = 784696, upload-time = "2026-08-15T02:48:09.314Z" }, - { url = "https://files.pythonhosted.org/packages/49/50/c77dca38074e38e99373ccd5f361bb5ad251e8f2de92dff4d7035111595d/hypothesis-6.165.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67a0c0346ac0e8e106fad0224269a87c665768cc8d45de6e367e9b260ef36d5a", size = 776265, upload-time = "2026-08-15T02:49:36.232Z" }, - { url = "https://files.pythonhosted.org/packages/1b/80/9de2e558cd966ab1b1d014718c7aaab7d2d8f2d591ccadb27c1ffe639f6d/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10e826a708456c965e125f15dbf5a6f463e41dcf230a930b7a3e0ce7036f5176", size = 1106706, upload-time = "2026-08-15T02:49:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/dc89684de1a739347b22beadf3e53a64b8b7eef287dfa34b150432e13d4e/hypothesis-6.165.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba20d9c60d6b8aec76f32cd22109e1b1f19a9985317255e2b43940d27996097", size = 1156753, upload-time = "2026-08-15T02:48:20.512Z" }, - { url = "https://files.pythonhosted.org/packages/b9/bc/495988f0fc788d33a24849e9ce7303a2c646f48baf3f5f010d9850bcc5a7/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6564fb9bbe1760719070a6fd37335fbf43696fe2911c386ba55fb3cbaaafb82a", size = 1280667, upload-time = "2026-08-15T02:48:30.763Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/9758ba15cc7dcb408d2699081ee0acbd96842f9c5778c84fd145d19953ba/hypothesis-6.165.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:126e7bf873892649cc5f697c3b3a6548f077a56ae3601b22fddef2ea1ba6f64d", size = 1324018, upload-time = "2026-08-15T02:48:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/97/09/eaef6ee89a792ea82064cef66726023a5933f8704c0d3dbfc5a451829428/hypothesis-6.165.9-cp312-cp312-win_amd64.whl", hash = "sha256:c908edd69e4308c3cb194f0853aa7dfaa15e905242e81646b9f12881cd2683d3", size = 672207, upload-time = "2026-08-15T02:49:50.121Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1f/99069da612aee3a51519f96f4370c4ba42055b57eae92926353c63e68617/hypothesis-6.165.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:11bcdfb14b52d41384cc14ecd3f6d6152735bc2f7af0b9f3fa8685d630a1e98d", size = 784591, upload-time = "2026-08-15T02:48:42.765Z" }, - { url = "https://files.pythonhosted.org/packages/94/10/cabf6a8782253e6b547ea8bdc6a48a9eeab415f1036c749b40ca59df32f8/hypothesis-6.165.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8c3d7d54802641c2f8b49f478f153567770d299d639ee38fc0a0fa3d95ee07cb", size = 776216, upload-time = "2026-08-15T02:49:34.179Z" }, - { url = "https://files.pythonhosted.org/packages/15/5f/da966f98d69a8ee4b4fac509d92f7ab18cb4505b8191acc23bbb52f8bb17/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8f5b63a8fba74d19a12ad4abaa95afb7466d66d1adc90ba3026e40b7924452c", size = 1106621, upload-time = "2026-08-15T02:49:08.529Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5d/1fffd1031b4281e53fff02ad7adeda31db8218e91b82e7a98c7957541e76/hypothesis-6.165.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c01b77160328d8fce22bb5121fe1f5fee36b434a1220603500605b88ab854d7a", size = 1156613, upload-time = "2026-08-15T02:47:45.457Z" }, - { url = "https://files.pythonhosted.org/packages/b1/30/8e93a02016689c0b5cf0c35f8f9199b745601b3db9723296ca6d7366dcd5/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88bde76aaab7f8abcd9412d135eaa49e8f7d89d068188fae2768a7803c80df89", size = 1280650, upload-time = "2026-08-15T02:49:44.026Z" }, - { url = "https://files.pythonhosted.org/packages/c0/48/2b0987389f141bd7bccc0785af4af86de9dfe9a6bc1b2b5dc612b083ae8a/hypothesis-6.165.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0df8f8736d176d3a5092890b6fb9030f9c726d272b52af0e1ce887d62fc21699", size = 1323749, upload-time = "2026-08-15T02:48:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a2/0a2380fdedfe0e16a09a09401b86bdc1eb049b3b6256fd837424d652c88e/hypothesis-6.165.9-cp313-cp313-win_amd64.whl", hash = "sha256:7fc9f46d88732fc74539ca7748aedc736c5e111431b06bd8ef2534440379aff2", size = 672195, upload-time = "2026-08-15T02:49:02.806Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d0/95075d5ae3263e1c9143a3e89d9f19b864b1da303fb426964d9f7c08ce69/hypothesis-6.165.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c3432a48e5895cba6b36c3e5abb915163d5169dc755685b4cd4720b710d77e3c", size = 784690, upload-time = "2026-08-15T02:49:23.922Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d9/4a8b58237a36a839abefac22905ce9172b4c5195344708690413c6ef900d/hypothesis-6.165.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70fe20939f63eb661227fd2b05193eba1e5ea6e547a2fbe158839f2acc519f4f", size = 776366, upload-time = "2026-08-15T02:48:27.261Z" }, - { url = "https://files.pythonhosted.org/packages/74/8d/91d1261f09a87299dc0ba89e12d8f9bbfd454e8922560e332b190a5c0a9f/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b177ef8aae2a2817a2223d103353d120118e686a152bcd01832a27ddc9388e", size = 1107128, upload-time = "2026-08-15T02:49:10.617Z" }, - { url = "https://files.pythonhosted.org/packages/2b/01/0198547c8b73d73ee5b885d8f73931c4dd3c62055dc93a80b5e18590efeb/hypothesis-6.165.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d755b56098801a28a419172e57beb536b8c32929e7a37bb5ef12f2c00abd284", size = 1156757, upload-time = "2026-08-15T02:48:25.44Z" }, - { url = "https://files.pythonhosted.org/packages/21/3c/a3bde8c9630d361d85570ed5a6248fec0360a3874e26fdb04d62c8ebabf9/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d49496271282bf424ddd728108f49b92e1d32066a3d0ba18990d5b01c40c3175", size = 1281013, upload-time = "2026-08-15T02:48:44.399Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/017895202e6f2d75928b8e66bb8efe28c5c834771f6a3d118fc936d811d0/hypothesis-6.165.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:be54673f76785ff954b511745d4a7a18c7d7fe1d893cb81a8917cd28878fbd55", size = 1324110, upload-time = "2026-08-15T02:49:18.05Z" }, - { url = "https://files.pythonhosted.org/packages/40/f8/02f9be17e21b55513da624bac18a21c29011351d4e5d089ca5095555d1b4/hypothesis-6.165.9-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:af42ad4c24b69d4482e4813e148f458fc090911c0cf9055225c62fbd1f0b102e", size = 616257, upload-time = "2026-08-15T02:49:00.997Z" }, - { url = "https://files.pythonhosted.org/packages/e9/be/c94631e799bb4ee87c8c80de72f2eb44e93e7d0bff45b22b8801c78e5895/hypothesis-6.165.9-cp314-cp314-win_amd64.whl", hash = "sha256:9bc2d40dd2a71ba3971b902d31c8b1b0c40d3d465591fd71bb8ce6f541add530", size = 672006, upload-time = "2026-08-15T02:48:32.418Z" }, - { url = "https://files.pythonhosted.org/packages/43/15/d23548a12e5670513903b73a42fbd4f5ab7b1287bc6fd5a4a890c5746a62/hypothesis-6.165.9-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fb16358e29f3090a9ab529e84df52923ddcd8505785c019bf48cdcd45e094a", size = 783152, upload-time = "2026-08-15T02:50:02.576Z" }, - { url = "https://files.pythonhosted.org/packages/0d/cd/37b5e5c042c557aa50db8c7f554b655b51c83e0b50306fb6ec8b05b03bfe/hypothesis-6.165.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ed61ad45184997f97c3baf255f34aaaf4edff63f1a61aad87919034b4ab9f64e", size = 774782, upload-time = "2026-08-15T02:49:47.988Z" }, - { url = "https://files.pythonhosted.org/packages/56/ed/43b6db3681937ffa191148533d37cdd3c091f707ecb92281424078876d45/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f96f36c3d3677f22ff990d7aab4235576a8429e72dab5382c45bf489630972f5", size = 1105367, upload-time = "2026-08-15T02:49:42.041Z" }, - { url = "https://files.pythonhosted.org/packages/0d/dd/eaab3c678a23ab434c65b4ea5de5b302267ce68aa47b996524d2f34dcd13/hypothesis-6.165.9-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69ab6500e47d7c21310360d1ee5cb6441660a10408e856d3738f07536732ed8", size = 1155499, upload-time = "2026-08-15T02:48:49.363Z" }, - { url = "https://files.pythonhosted.org/packages/10/fa/27049cbfb3e66cc1b68b32cbe137ac69a91cf09ac8520c4eb8ae9b3e5cbe/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cc98fa897f48127fdf2e7d1b2428f839656cd52a6344095e501371d222b9c19d", size = 1279038, upload-time = "2026-08-15T02:49:26.001Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d4/ab591a445dfeb7fa59b3c12de3eb9ff09901b4ede4025061b2fe1bc302af/hypothesis-6.165.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a8b2d7415b1f7ceec6e689c992b919af41e339eafb35d6367a3abbb5b6db9753", size = 1322697, upload-time = "2026-08-15T02:48:39.467Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/4ee3b14d2d73a1d9442ac161b953dea2522fdac8ef7ca265fe5dd205982d/hypothesis-6.165.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1000afaf05be59c88c13b458fd74d123e3361ca3d14fe127814a1231cb9cb7df", size = 672011, upload-time = "2026-08-15T02:48:07.583Z" }, - { url = "https://files.pythonhosted.org/packages/81/a1/20b63e3d17e05e82ef47b93ea3fa9a823297249ed7953e7e38f1b57a1e91/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:5f6b4ae650480368e2477e8f875cdbfe8533a7b2e09f049fe206c48b20174622", size = 782729, upload-time = "2026-08-15T02:49:52.008Z" }, - { url = "https://files.pythonhosted.org/packages/7f/13/b43cea9578820a3e41cd70e8395068a0f07ef11c393ab09ff5aeb5d44d7f/hypothesis-6.165.9-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:1aaba05b943345d2a448b8c1c795bb191adf842b450bfc7207bdd1bf139a9b58", size = 774415, upload-time = "2026-08-15T02:47:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/ea/08/222a8f2aeb8429b54cd53624add076ae5c9b39b0b170afd2435d0e81a8a9/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:306c1efc0f01f3d5c29c45996e055437971337aa24b6c6735b0514cfb5df60ce", size = 1104761, upload-time = "2026-08-15T02:47:46.841Z" }, - { url = "https://files.pythonhosted.org/packages/68/6b/09943bad5c1c5ea689a1be6376bcb7c68c34f3e3db5b792a1c0bb4edd79c/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1551df11534d192616dc1b4539b6e526c4704d08b22d2ffb6ea3479ce6b87d94", size = 1133212, upload-time = "2026-08-15T02:47:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ee/8fa90ca7a7d8b2362f3aaa0b2117405abf7b104fa01b52d5fecc4bb3c3fc/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afd99a069df90c73c15d53e275d11a782fd5f17f5423002868ce9d2438ab567f", size = 1132156, upload-time = "2026-08-15T02:47:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/6dae83eccd7260f5cf3468f145c663ce9b22579a9d9c80e9e7be49bec8b2/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c3e8c45ca5e2e4f519021836e5d86fb2229915c3f92fb50db7bb7364dfb2c1", size = 1155040, upload-time = "2026-08-15T02:49:16.085Z" }, - { url = "https://files.pythonhosted.org/packages/50/90/25eaabe2fecb3af3bcb9b4c3261ba9ffe0e092a3199232adce15399c040b/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:676a3bd04261b7bdd35374a3f4dabd82025fcead110a067a76328799f394a4d0", size = 1109751, upload-time = "2026-08-15T02:50:00.293Z" }, - { url = "https://files.pythonhosted.org/packages/1f/81/e8117e7ed3ff1ef7a36f023469efc925458b52c1243c539566a5308f5d77/hypothesis-6.165.9-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7f51c2297d116c2c025643762269eed0767589b6fc858d8de69d9f80cf33a23", size = 1144793, upload-time = "2026-08-15T02:49:38.317Z" }, - { url = "https://files.pythonhosted.org/packages/c6/6a/da2ea69d1fed780d86895f1ef7f2811240ed810487536ad22298629cc630/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e500b97f899e375e47b407a48f3450cb1dd44bca757529c0490cb4533d878b30", size = 1278531, upload-time = "2026-08-15T02:48:37.581Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6e/b3b21f15fe9debb9c448b03aaeb725ee007ba68518e55a6810878d7b9765/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8401f55aba2d51f20b02c42e9e08b7eff2018639bdb1af16a4c279ba7e129c81", size = 1407073, upload-time = "2026-08-15T02:48:41.256Z" }, - { url = "https://files.pythonhosted.org/packages/da/fe/55f8e9c34f2b0a5ccf895a527a499f8562e9a14f014dd018b2535e8aa1c4/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:34a4591db1dcf6aa544b4f884520ff0f1bc02ac3d11c76f78c73535b6b672272", size = 1261243, upload-time = "2026-08-15T02:49:58.076Z" }, - { url = "https://files.pythonhosted.org/packages/0b/37/49fb2374bff4e66d3d8264d083ddbb83269ec13903b441ebbc0d3c51502a/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:0bd589a011b3d0c4b98d78f467e5a5a6b96bf5be5d15b39ef47e65c225e8d790", size = 1279060, upload-time = "2026-08-15T02:48:54.843Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b7/c826e67f5d553d20644ac3da5d25de7ff9d50a1f41467981b00da7b2ab9b/hypothesis-6.165.9-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:84a93a152ffaf92fa6b3f94d366f4c866e26f3f1b643fcc6da4ee2ddfa038aa9", size = 1322247, upload-time = "2026-08-15T02:47:54.398Z" }, - { url = "https://files.pythonhosted.org/packages/24/ec/e70e440805820cfd662d04d05aa34b5d29e46d5a49ee871d6937721b2c17/hypothesis-6.165.9-cp315-abi3.abi3t-win32.whl", hash = "sha256:4a2081699e0b16b35c7b34cf0ff01997e8fc6038b520b9f29c106764158c411a", size = 665906, upload-time = "2026-08-15T02:49:53.901Z" }, - { url = "https://files.pythonhosted.org/packages/23/79/968b149b572e1d4284b04280cc181449090c8bd03e55b24d57b64443978e/hypothesis-6.165.9-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:e1d9492193b52087df4d264edd77618869d447b5c7f6a022c7bb11b88d577cb8", size = 671817, upload-time = "2026-08-15T02:49:46.053Z" }, - { url = "https://files.pythonhosted.org/packages/c2/72/0f951d4f0e07720583e4773638cd556d2b6585605f8a3df54d2fecf2e2ff/hypothesis-6.165.9-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:f67414675f2263948e5a5977b636b5cb1cc5f33b82a9be34f8a6b9a8293d4707", size = 669795, upload-time = "2026-08-15T02:48:03.176Z" }, - { url = "https://files.pythonhosted.org/packages/43/7d/1b6b48629032b22960a5b010a791467670c08d9413a3c6c15a72f7c1da09/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f6caa4194f37a02232ecb964600d17facc23a6aa5b0295008989f31e329eb837", size = 784507, upload-time = "2026-08-15T02:48:45.961Z" }, - { url = "https://files.pythonhosted.org/packages/95/a6/c29939a55cde73886cee7a3975148e132a9ed6881d39b7c9b8c19e3a854b/hypothesis-6.165.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0a4a32dc8e8728417aa13055903dbf742dd0b5cedda7c2031b4b2a0bcc1d463d", size = 780353, upload-time = "2026-08-15T02:49:14.307Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f9/ac8cd423b1c7506dae01c4a9295c434b53525401a182a116aecaa27332cf/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f255b389364f71e6e9305ba99bf13733354cb0dc4a97d07db8aef76278a345e", size = 1109224, upload-time = "2026-08-15T02:48:15.557Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/64c4dfa2836ec421e35a167a3fba92018950a8c7551a268a5fc799329455/hypothesis-6.165.9-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7f0507d8224e5b44c2651d7ef8df15ac6d38eb56c98e7f451c3090804ce109", size = 1158929, upload-time = "2026-08-15T02:49:21.922Z" }, - { url = "https://files.pythonhosted.org/packages/88/8e/fccbea8a0a03532762fdd56d3809fd8398f46af1ec9018f694d4ec5c22cf/hypothesis-6.165.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fd9e242fabb05715ec2d3b9b96dcddf683fc3891a04b6685b79e400e35ccbe18", size = 675885, upload-time = "2026-08-15T02:47:50.986Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/9a9538e6d185baf5cc7f15bc3b76e08efbb3de4b3c782f234356449c0dd7/hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f", size = 783243, upload-time = "2026-08-16T22:55:44.058Z" }, + { url = "https://files.pythonhosted.org/packages/a1/30/b70d9d79e871a75cbdeccd9067f20ecdb9eb2a1dfa03c630be3ad13b8b30/hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd", size = 778815, upload-time = "2026-08-16T22:55:46.948Z" }, + { url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" }, + { url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" }, + { url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/99/27450763853a034bca1574d3e0a315164b33ff49c3862df6872dda45e25e/hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746", size = 669039, upload-time = "2026-08-16T22:55:11.962Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fc/ff2988b72b5705ad9ca500444bf3f43e3c2f41edfa034bbfeb23b215791a/hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2", size = 675213, upload-time = "2026-08-16T22:55:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8b/821810d36f78d9d9421cd2c5d9d36983b45bb3575c3086276cc5c76f9f73/hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1", size = 673537, upload-time = "2026-08-16T22:54:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/26/61/5e89268ce03317fb9f82449a1b3efd9e599dee090288fd0cf7586c532fb1/hypothesis-6.165.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8", size = 783959, upload-time = "2026-08-16T22:55:29.078Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e9/f4e0832e81bb53b70cf1712e28c867db64245b32595b594217452e7dbd8d/hypothesis-6.165.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10", size = 779684, upload-time = "2026-08-16T22:54:57.698Z" }, + { url = "https://files.pythonhosted.org/packages/77/de/ea072d3359d5678771bed407f80439e8ac7ca905d1031b0372f61bf5746e/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65", size = 1108540, upload-time = "2026-08-16T22:55:03.282Z" }, + { url = "https://files.pythonhosted.org/packages/28/56/e7c395cdaa3d6c28b944c1c3c516dee50d2b7b3aeafa31874b57009ca51f/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131", size = 1158089, upload-time = "2026-08-16T22:54:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/1c6d2c465b9c5fc3213f1be89be95ba53819ca0130248c484129ccfefb71/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b", size = 1284125, upload-time = "2026-08-16T22:54:37.727Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bc/7caf5ac3d0173bd57bd2a5ab854ca49a3664a4309257be1452f81025cc24/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6", size = 1325082, upload-time = "2026-08-16T22:54:38.952Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/9d844330f570d6a4f127a683eab1e78c8263e6e72b16189f3534fa6bf6de/hypothesis-6.165.10-cp310-cp310-win_amd64.whl", hash = "sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72", size = 675082, upload-time = "2026-08-16T22:54:29.872Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c2/b9546ace11f241c9c02d389f258cb80c14447a8c885771c9f1f0bc1d85ca/hypothesis-6.165.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1", size = 783716, upload-time = "2026-08-16T22:55:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/37/10/27c2fdd574fd798caf5e91eb51f7834b098f5d840ce733efb3fba79ef86e/hypothesis-6.165.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5", size = 779507, upload-time = "2026-08-16T22:55:07.633Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/70bc23695f3783c4b0486b6cad47b08a20f791db4a3c1b25250add9659fa/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d", size = 1108406, upload-time = "2026-08-16T22:55:39.653Z" }, + { url = "https://files.pythonhosted.org/packages/71/4c/32e200bd7a352af4b7f4e3729aaa4cd002cb5fe8c4c6aef5599d0019f152/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589", size = 1157850, upload-time = "2026-08-16T22:55:24.394Z" }, + { url = "https://files.pythonhosted.org/packages/03/a5/8efc2a9a484822efc0d0da466f50094e0f2c068187faaf33831fc905873e/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1", size = 1283704, upload-time = "2026-08-16T22:54:27.279Z" }, + { url = "https://files.pythonhosted.org/packages/46/2a/90cc8d7463929c04786f29600de45f3227c12fa9bed1d5b7ce319b05e1c9/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2", size = 1325077, upload-time = "2026-08-16T22:55:16.561Z" }, + { url = "https://files.pythonhosted.org/packages/82/ac/bc16faba4b42883e3d290bfaceff51e258b63fbbdf789bf9fe88df1ce537/hypothesis-6.165.10-cp311-cp311-win_amd64.whl", hash = "sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e", size = 674920, upload-time = "2026-08-16T22:55:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/e9/45/cde4f78afe2b9e29caecf38319eedc1deb76aebcacbdd128e03cbb2511c3/hypothesis-6.165.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94", size = 784835, upload-time = "2026-08-16T22:54:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/847f30b81cbfd07607296b3ce43067cf4f80799bd9244167f587de9c8081/hypothesis-6.165.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45", size = 776419, upload-time = "2026-08-16T22:55:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/04/66/4c71c5be7a49d84b8c3a9278c1807c4c81181ab5474beb27df9d4c40dc0e/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f", size = 1106830, upload-time = "2026-08-16T22:55:10.389Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/e2cbd2810e79f7a452a8ea9f6c6438ee718ce938d8cc12252cf0b36a81d3/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28", size = 1156952, upload-time = "2026-08-16T22:55:53.35Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/794ced36864825492ac3712d5acab5a257b4601e6a9dc2ccdd3937198f87/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81", size = 1280780, upload-time = "2026-08-16T22:54:34.983Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2d/550525442cdbcc2daf1f9bdd8ba35bcbde63db7c7a22f2ef137fbb49df2f/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f", size = 1324130, upload-time = "2026-08-16T22:55:48.659Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/6caf69dd5fe03499ada94c9cec016bffcc164511c6b93fe680f01209b9ff/hypothesis-6.165.10-cp312-cp312-win_amd64.whl", hash = "sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9", size = 672337, upload-time = "2026-08-16T22:54:49.11Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fb/c82c5bd92864ffcf319772fedc8c9bf2dbe4ca14baa0fee6e49e67b5ba1c/hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad", size = 784726, upload-time = "2026-08-16T22:54:32.371Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b9/3d7acd08506da85557e65147b7f3fca8c47684e33be90bee0acb523920db/hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4", size = 776375, upload-time = "2026-08-16T22:55:13.303Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/662b94880f260b0a88de1fdcf60fc9984f6e2a796da549542adc10a7bc83/hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747", size = 672346, upload-time = "2026-08-16T22:56:03.792Z" }, + { url = "https://files.pythonhosted.org/packages/3f/77/55e020c9c576532ff7d20bf8b1dfa052ecbd5ada1949b02f76c44c966f7e/hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209", size = 784833, upload-time = "2026-08-16T22:55:21.255Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f2/01da2adf829cf549eaddcabb8e8072077fb3d26da4275f4c1e89b2c0af74/hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611", size = 776545, upload-time = "2026-08-16T22:56:10.159Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" }, + { url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/86/9b4fb75f520a028edec50ffc904a94d724180395d71feb6d7a0ce7bb6f00/hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd", size = 672145, upload-time = "2026-08-16T22:54:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ba/f7bbaae0c789bab7ddb764d2056ee1a463cc95a8acbccc90d4184e48b242/hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff", size = 783287, upload-time = "2026-08-16T22:54:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/3a/83/01ef80772b4abd335c49405576dc503cede94fb5da30ba2643a119013aea/hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4", size = 774991, upload-time = "2026-08-16T22:55:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" }, + { url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" }, + { url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e7/b0a2fde7570c090a1b914026266a421c751ef10138fffe37fe0ef9e675c0/hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8", size = 672147, upload-time = "2026-08-16T22:55:27.527Z" }, + { url = "https://files.pythonhosted.org/packages/47/fd/985aa564d6ffd06483d45a62b40d319df0a703cd8bc1d041de17d102fbaa/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5", size = 782882, upload-time = "2026-08-16T22:55:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2c/6cc11151e450f72353a490940cd0db704680d07b78dc75dcc9f480e0d0e1/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a", size = 774584, upload-time = "2026-08-16T22:55:51.822Z" }, + { url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" }, + { url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/55/63/ad5cc153dcc72ae5e7905fb9b3585f3e48ce892a2d6366f90163e867a69d/hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc", size = 666038, upload-time = "2026-08-16T22:56:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/80/32/b62307b73fbc99f0a4381d6f9456df76fbcbb7a27ef7256e26f0376f48ea/hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d", size = 671941, upload-time = "2026-08-16T22:55:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dd/e0f98add0548ef73ea7afac45da1fb8efc854d7f9931db568754d0f963f3/hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015", size = 669931, upload-time = "2026-08-16T22:55:50.205Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6a/880d6eeed5c451fb40a66733dadec4a5d498628a4a7f6a8a5f633f4c6dcb/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802", size = 784644, upload-time = "2026-08-16T22:54:20.127Z" }, + { url = "https://files.pythonhosted.org/packages/27/e0/9e942bd3c3cf5ea0d5c0fd0905893bbfb6cefb7284c70fcc8033f8fdec38/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0", size = 780515, upload-time = "2026-08-16T22:55:04.676Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/f11a618415dc5fa9cdde41fea56c489f0814759527ae1ecd11a75a4558b9/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9", size = 1109374, upload-time = "2026-08-16T22:56:00.241Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6f/db49b719842297c2b71e0d81e5b8967d31215fb7389421abcb465ce7ed3f/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4", size = 1159092, upload-time = "2026-08-16T22:55:58.57Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/bf0bae84ba1cb3923d295973f1fe38ee867eaf90119e0d559116083be300/hypothesis-6.165.10-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757", size = 676045, upload-time = "2026-08-16T22:55:45.514Z" }, ] [[package]] From c257e01bd6f4257c6437d2a84ffb2fbad462a87f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:51:28 +0200 Subject: [PATCH 007/322] chore: Bump rollup from 4.62.4 to 4.62.5 in the all-npm group (#4910) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 208 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 105 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82a40edb54..38749e54dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.4", + "rollup": "^4.62.5", "terser": "^5.50.0", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", - "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", - "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", - "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", - "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", - "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", - "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", - "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", - "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", - "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", - "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", - "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", - "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", - "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", - "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", - "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", - "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", - "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", - "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", - "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", - "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", - "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", - "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", - "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", - "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", - "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", "cpu": [ "x64" ], @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.62.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", - "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.4", - "@rollup/rollup-android-arm64": "4.62.4", - "@rollup/rollup-darwin-arm64": "4.62.4", - "@rollup/rollup-darwin-x64": "4.62.4", - "@rollup/rollup-freebsd-arm64": "4.62.4", - "@rollup/rollup-freebsd-x64": "4.62.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", - "@rollup/rollup-linux-arm-musleabihf": "4.62.4", - "@rollup/rollup-linux-arm64-gnu": "4.62.4", - "@rollup/rollup-linux-arm64-musl": "4.62.4", - "@rollup/rollup-linux-loong64-gnu": "4.62.4", - "@rollup/rollup-linux-loong64-musl": "4.62.4", - "@rollup/rollup-linux-ppc64-gnu": "4.62.4", - "@rollup/rollup-linux-ppc64-musl": "4.62.4", - "@rollup/rollup-linux-riscv64-gnu": "4.62.4", - "@rollup/rollup-linux-riscv64-musl": "4.62.4", - "@rollup/rollup-linux-s390x-gnu": "4.62.4", - "@rollup/rollup-linux-x64-gnu": "4.62.4", - "@rollup/rollup-linux-x64-musl": "4.62.4", - "@rollup/rollup-openbsd-x64": "4.62.4", - "@rollup/rollup-openharmony-arm64": "4.62.4", - "@rollup/rollup-win32-arm64-msvc": "4.62.4", - "@rollup/rollup-win32-ia32-msvc": "4.62.4", - "@rollup/rollup-win32-x64-gnu": "4.62.4", - "@rollup/rollup-win32-x64-msvc": "4.62.4", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index a572ab555f..a35f6a4bae 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.4", + "rollup": "^4.62.5", "terser": "^5.50.0", "typescript": "^7.0.2" }, From 27dd05bcd344d47e48fae4052e855a36ec0b6086 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:52:03 +0200 Subject: [PATCH 008/322] chore: Bump @testing-library/user-event from 14.6.4 to 14.6.6 in /tests/React in the all-npm group (#4909) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 8 ++++---- tests/React/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index e5df4a5130..fb39b0a4e3 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -10,7 +10,7 @@ "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.4", + "@testing-library/user-event": "^14.6.6", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", @@ -1538,9 +1538,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", - "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", "license": "MIT", "engines": { "node": ">=12", diff --git a/tests/React/package.json b/tests/React/package.json index 1099726075..d45fcc8a26 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -12,7 +12,7 @@ "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.4", + "@testing-library/user-event": "^14.6.6", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", From 46ac68998554243b9e228eee69cfb30baad8f628 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:52:27 +0200 Subject: [PATCH 009/322] chore: Bump the all-actions group with 2 updates (#4912) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index d1fbc0eade..4592c2aa87 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup-cli@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 8a2253d099..151ac7ba16 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -51,7 +51,7 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 +# - github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@b77e0d501fd2d2243d1f72722617e80d513f674e # v0.87.0 + uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 29 Aug 2026 15:47:15 +0000 Subject: [PATCH 010/322] fix(rust): support `string` on a tuple (#4916) --- src/Fable.Transforms/Rust/Replacements.fs | 16 +++++++++++++++- tests/Rust/tests/src/TupleTests.fs | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 448e3f2a60..93d33cb450 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -265,7 +265,7 @@ let convertTo com (ctx: Context) r t (args: Expr list) = addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) -let toString com (ctx: Context) r (args: Expr list) = +let rec toString com (ctx: Context) r (args: Expr list) = match args with | [] -> "toString is called with empty args" @@ -275,6 +275,20 @@ let toString com (ctx: Context) r (args: Expr list) = | String -> head | Char -> Helper.LibCall(com, "String", "ofChar", String, [ head ]) | Boolean -> Helper.LibCall(com, "String", "ofBoolean", String, [ head ]) + // Rust has no Display for tuples, and the orphan rule rules out adding + // one. Building the text here also matches .NET more closely than a + // derived Debug would: each element goes through its own ToString, so a + // string element is unquoted and a bool prints as True/False. + | Tuple(genArgs, _) when not (List.isEmpty genArgs) -> + let element i genArg = + toString com ctx r [ Get(head, TupleIndex i, genArg, r) ] + + let separated = + genArgs + |> List.mapi element + |> List.reduce (fun acc part -> add (add acc (makeStrConst ", ")) part) + + add (add (makeStrConst "(") separated) (makeStrConst ")") | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toString", String, args) | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toString", String, args) // | Array _ | List _ -> diff --git a/tests/Rust/tests/src/TupleTests.fs b/tests/Rust/tests/src/TupleTests.fs index ef0d9938a2..85f3db2446 100644 --- a/tests/Rust/tests/src/TupleTests.fs +++ b/tests/Rust/tests/src/TupleTests.fs @@ -103,3 +103,13 @@ let ``Can create tuples of single element`` () = let t1b = Tuple.Create "b" t1.Item1 |> equal 4 t1b.Item1 |> equal "b" + +// Rust has no Display for tuples and the orphan rule rules out adding one, so +// `string` on a tuple did not compile. Building the text from each element's own +// ToString also matches .NET, where a string element is unquoted and a bool +// prints as True -- neither of which a derived Debug would give. +[] +let ``string on a tuple matches .NET`` () = + string (1, 2) |> equal "(1, 2)" + string (1, "a", true) |> equal "(1, a, True)" + string ((1, 2), 3) |> equal "((1, 2), 3)" From 584024712450d333193baddc239db977af05bd54 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 29 Aug 2026 15:51:36 +0000 Subject: [PATCH 011/322] fix(rust): decimal culture overloads, and mutables assigned in try/finally (#4918) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 31 ++++++++++++++ src/Fable.Transforms/Rust/Replacements.fs | 38 ++++++++++++++++++ tests/Rust/tests/src/ControlFlowTests.fs | 49 +++++++++++++++++++++++ tests/Rust/tests/src/ConvertTests.fs | 26 ++++++++++++ 4 files changed, 144 insertions(+) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 3a2a3d2893..a786a03344 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3208,8 +3208,39 @@ module Util = let addCapturedNames expr = capturedNames.UnionWith(FableTransforms.getCapturedNames expr) + // A try/with/finally lowers to closures on this target, but the Fable AST + // has no Lambda there, so the shared capture walk does not see those + // bodies as closure contexts. Everything they reference is captured all + // the same: without this a `let mutable` assigned in a finally block was + // emitted as a bare MutCell, and the closure mutated a clone of it. + let addTryCatchCaptures expr = + expr + |> deepExists (fun e -> + match e with + | Fable.TryCatch(body, catch, finalizer, _) -> + [ Some body; catch |> Option.map snd; finalizer ] + |> List.choose id + |> List.iter (fun part -> + part + |> deepExists (fun inner -> + match inner with + | Fable.IdentExpr ident -> + capturedNames.Add ident.Name |> ignore + false + | _ -> false + ) + |> ignore + ) + + false + | _ -> false + ) + |> ignore + bindings |> List.iter (snd >> addCapturedNames) addCapturedNames letBody + bindings |> List.iter (snd >> addTryCatchCaptures) + addTryCatchCaptures letBody capturedNames let getScopedIdentCtx com ctx (ident: Fable.Ident) isArm isRef isBox isFunc usages = diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 93d33cb450..8cd6dfb731 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -2235,7 +2235,37 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args | _ -> None +/// Drops culture and style arguments from a call. +/// +/// Rust's formatting and parsing are invariant, which is exactly what +/// InvariantCulture asks for, so the overloads that take a provider are +/// equivalent to the ones that do not. Passing the argument on was not: the +/// runtime functions have no parameter for it, so `Decimal.Parse(s, style, +/// culture)` reached a one-argument function and `d.ToString(culture)` put a +/// provider where a format string belongs. +/// +/// Filtering by argument type rather than by position leaves the arity of every +/// other overload alone. +let dropCultureArgs (args: Expr list) = + let isCultureOrStyle (e: Expr) = + match e.Type with + | DeclaredType(ent, _) -> + match ent.FullName with + | "System.Globalization.CultureInfo" + | "System.IFormatProvider" -> true + | _ -> false + | Number(_, NumberInfo.IsEnum ent) -> + match ent.FullName with + | "System.Globalization.NumberStyles" + | "System.Globalization.DateTimeStyles" -> true + | _ -> false + | _ -> false + + args |> List.filter (isCultureOrStyle >> not) + let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let args = dropCultureArgs args + match i.CompiledName, args with | (".ctor" | "MakeDecimal"), ([ low; mid; high; isNegative; scale ] as args) -> Helper.LibCall(com, "Decimal", "fromParts", t, args, i.SignatureArgTypes, ?loc = r) @@ -2295,6 +2325,14 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: // let format = makeStrConst ("{0:" + rustFmt + "}") // "sprintf!" |> emitFormat com r t [format; thisArg.Value] |> Some | "ToString", _ -> + // For d.ToString(provider) the provider is the only argument, so dropping + // it leaves nothing for toString's decimal parameter; the receiver takes + // its place. d.ToString() already arrives with the receiver in args. + let args = + match args, thisArg with + | [], Some this -> [ this ] + | _ -> args + Helper.LibCall(com, "Decimal", "toString", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args diff --git a/tests/Rust/tests/src/ControlFlowTests.fs b/tests/Rust/tests/src/ControlFlowTests.fs index a66a6bce17..66e345ced8 100644 --- a/tests/Rust/tests/src/ControlFlowTests.fs +++ b/tests/Rust/tests/src/ControlFlowTests.fs @@ -183,3 +183,52 @@ let ``two-switch or-pattern with obj binding works`` () = // exercises the getZeroObj placeholder for a `dyn Any` binding on the two-switch path orPatObj (OA(box 9)) :? int |> equal true orPatObj (OB(box 11)) :? string |> equal false + +// A try/with/finally lowers to closures on this target, but the Fable AST has no +// Lambda there, so the capture walk did not treat those bodies as closure +// contexts. A `let mutable` assigned in a finally block was emitted as a bare +// MutCell and the closure mutated a clone, so the write was silently lost. +// +// The exception-free case runs everywhere; the ones below need a `with` that +// actually catches, which no_std's try_catch does not do. +[] +let ``a mutable assigned in a finally block keeps its value`` () = + let mutable ran = false + + let message = + try + "body" + finally + ran <- true + + ran |> equal true + message |> equal "body" + +#if !NO_STD_NO_EXCEPTIONS +[] +let ``a mutable assigned in a finally block survives an exception`` () = + let mutable ran = false + + let message = + try + try + failwith "inner" + finally + ran <- true + with ex -> + ex.Message + + ran |> equal true + message |> equal "inner" + +[] +let ``a mutable assigned in a with handler keeps its value`` () = + let mutable ran = false + + try + failwith "x" + with _ -> + ran <- true + + ran |> equal true +#endif diff --git a/tests/Rust/tests/src/ConvertTests.fs b/tests/Rust/tests/src/ConvertTests.fs index 33f69d37de..cd71ebf032 100644 --- a/tests/Rust/tests/src/ConvertTests.fs +++ b/tests/Rust/tests/src/ConvertTests.fs @@ -1584,3 +1584,29 @@ let ``System.Convert.ToFloat from bool works`` () = Convert.ToSingle(false) |> equal 0.f Convert.ToDouble(true) |> equal 1. Convert.ToDouble(false) |> equal 0. + +// The culture-taking overloads used to fail to compile: the provider was passed +// on to runtime functions that have no parameter for it, so Decimal.Parse(s, +// style, culture) reached a one-argument function and d.ToString(culture) put a +// provider where the decimal belongs. Rust formatting and parsing are invariant, +// which is what InvariantCulture asks for, so the argument is dropped. +[] +let ``culture-taking overloads work`` () = + let d = 1234.5678M + d.ToString(System.Globalization.CultureInfo.InvariantCulture) |> equal "1234.5678" + d.ToString() |> equal "1234.5678" + + System.Decimal.Parse( + "1234.5678", + System.Globalization.NumberStyles.Number, + System.Globalization.CultureInfo.InvariantCulture + ) + |> equal 1234.5678M + + System.Decimal.Parse("12.34", System.Globalization.CultureInfo.InvariantCulture) + |> equal 12.34M + + System.DateTime + .Parse("2026-08-27T10:30:00.0000000", System.Globalization.CultureInfo.InvariantCulture) + .ToString("o") + |> equal "2026-08-27T10:30:00.0000000" From 395c152bd4ef0c26af7a4ff48ed44c728aa73d25 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 29 Aug 2026 15:54:54 +0000 Subject: [PATCH 012/322] fix(rust): let a string be enumerated (#4921) --- src/Fable.Transforms/Rust/Replacements.fs | 5 ++++ tests/Rust/tests/src/StringTests.fs | 28 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 8cd6dfb731..21195cf623 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -1389,6 +1389,11 @@ let getEnumerator com r t i (expr: Expr) = // | IsEntity (Types.regexCaptureCollection) _ | Array _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ expr ], ?loc = r) | List _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ expr ], ?loc = r) + // A string is enumerable in F# but `string` is `LrcStr` in Rust, with no + // GetEnumerator to fall through to -- so `for ch in s do` did not compile. + | String -> + let ar = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) + Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) | IsEntity (Types.hashset) _ | IsEntity (Types.iset) _ -> let ar = Helper.LibCall(com, "HashSet", "entries", t, [ expr ]) diff --git a/tests/Rust/tests/src/StringTests.fs b/tests/Rust/tests/src/StringTests.fs index 7f67365f7a..c6387e885f 100644 --- a/tests/Rust/tests/src/StringTests.fs +++ b/tests/Rust/tests/src/StringTests.fs @@ -1590,3 +1590,31 @@ let ``String.filter with Char.IsDigit as a predicate doesn't hang`` () = // let s3: FormattableString = $"""I have no holes""" // s3.GetStrings() |> equal [|"I have no holes"|] // #endif + +// A string is enumerable in F#, but `string` is `LrcStr` in Rust with no +// GetEnumerator to fall through to, so `for ch in s do` did not compile. +[] +let ``for-in over a string works`` () = + let mutable count = 0 + let mutable upper = "" + + for ch in "abc" do + count <- count + 1 + upper <- upper + string (System.Char.ToUpper ch) + + count |> equal 3 + upper |> equal "ABC" + +[] +let ``for-in over an empty string does nothing`` () = + let mutable count = 0 + + for _ in "" do + count <- count + 1 + + count |> equal 0 + +[] +let ``a string works with the Seq functions`` () = + "hello" |> Seq.filter (fun c -> c = 'l') |> Seq.length |> equal 2 + "hello" |> Seq.toList |> List.length |> equal 5 From 88135b90382f63bd40d58dfc75c4f915cc4bb868 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 29 Aug 2026 16:47:14 +0000 Subject: [PATCH 013/322] fix(rust): Async perf: stop the async runtime sleeping, and add Parallel (#4920) --- src/fable-library-rust/src/Async.rs | 366 +++++++++++++++++++----- src/fable-library-rust/src/Exception.rs | 32 ++- tests/Rust/tests/src/AsyncTests.fs | 219 +++++++++++++- 3 files changed, 532 insertions(+), 85 deletions(-) diff --git a/src/fable-library-rust/src/Async.rs b/src/fable-library-rust/src/Async.rs index bd539baeaa..1f104d3d7f 100644 --- a/src/fable-library-rust/src/Async.rs +++ b/src/fable-library-rust/src/Async.rs @@ -12,10 +12,35 @@ pub mod Async_ { use futures_timer::Delay; use super::Task_::Task; + use crate::NativeArray_::{array_from, Array}; + use crate::Native_::{Func0, Seq}; use crate::System::Threading::CancellationToken; pub struct Async { pub future: Arc + Send + Sync>>>>, + + /// How to build this computation again, where there is such a thing. + /// + /// An F# `Async` is a description that can be run any number of + /// times; a Rust `Future` can be polled to completion exactly once. + /// `while` and `for` inside an async block have to run their body once + /// per iteration, so `AsyncBuilder_::delay` -- the one place the F# + /// compiler hands over a re-runnable description, and which wraps every + /// loop body -- keeps its builder here. Everything else leaves it None + /// and stays single-shot exactly as before. + pub restart: Option>>>, + } + + impl Async { + /// Wraps a future as a single-shot computation. + pub fn from_future(fut: impl Future + Send + Sync + 'static) -> Arc> { + let b: Pin + Send + Sync + 'static>> = Box::pin(fut); + + Arc::from(Async { + future: Arc::from(Mutex::from(b)), + restart: None, + }) + } } impl Future for &Async { @@ -25,27 +50,20 @@ pub mod Async_ { self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - let p = self - .future - .try_lock() - .map(|mut f| f.poll_unpin(cx)) - .unwrap_or_else(|| { - //Again blocking wait, not good - thread::sleep(Duration::from_millis(10)); - cx.waker().wake_by_ref(); - - std::task::Poll::Pending - }); - p + // Poll the lock's own future so contention parks on its waker. + // try_lock plus a 10ms sleep spent that long doing nothing whenever + // two pollers met. + let mut lock = self.future.lock(); + + match lock.poll_unpin(cx) { + std::task::Poll::Ready(mut guard) => guard.as_mut().poll(cx), + std::task::Poll::Pending => std::task::Poll::Pending, + } } } pub fn sleep(milliseconds: i32) -> Arc> { - let fut = Delay::new(Duration::from_millis(milliseconds as u64)); - let a: Pin + Send + Sync + 'static>> = Box::pin(fut); - Arc::from(Async { - future: Arc::from(Mutex::from(a)), - }) + Async::from_future(Delay::new(Duration::from_millis(milliseconds as u64))) } pub fn startAsTask( @@ -76,30 +94,245 @@ pub mod Async_ { executor::block_on(unitFut) } + /// Async.Parallel. Runs every computation on one executor and collects the + /// results in order. Previously missing entirely, so any use of it failed to + /// resolve. + pub fn parallel( + computations: Seq>>, + ) -> Arc>> { + // F# hands this a seq, so materialise it before anything is awaited. + let items: Vec>> = crate::Seq_::toArray(computations).iter().cloned().collect(); + + let fut = async move { + let futures = items.into_iter().map(|a| async move { + let mut guard = a.future.lock().await; + guard.as_mut().await + }); + + let results: Vec = futures::future::join_all(futures).await; + array_from(results) + }; + + Async::from_future(fut) + } + + /// Async.StartImmediateAsTask. Runs on the calling thread rather than + /// queueing onto the thread pool, so it does not pay for a hand-off. The + /// returned task is therefore already complete: there is no scheduler here + /// to leave it pending on, so this trades .NET's concurrency for the speed + /// the name promises. Use startAsTask when the work should overlap. + pub fn startImmediateAsTask( + a: Arc>, + cancellationToken: Option, + ) -> Arc> { + let result = runSynchronously(a, None, cancellationToken); + Arc::from(Task::from_result(result)) + } + pub fn awaitTask(a: Arc>) -> Arc> { - let fut = async move { (&*a).await }; - let a: Pin + Send + Sync + 'static>> = Box::pin(fut); - Arc::from(Async { - future: Arc::from(Mutex::from(a)), - }) + Async::from_future(async move { (&*a).await }) } } #[cfg(feature = "threaded")] pub mod AsyncBuilder_ { use std::future::{Future, ready}; + use std::panic::AssertUnwindSafe; use std::pin::Pin; use std::sync::Arc; + use futures::FutureExt; use futures::lock::Mutex; use super::Async_::Async; - use crate::Native_::{Func0, Func1}; + use crate::Exception_::get_exception; + use crate::Native_::{eraseUnitArg, EraseUnitArg, Func0, Func1, LrcPtr, Seq}; + use crate::System::Exception; + + /// Awaits one computation to completion. + /// + /// Every method here goes through this rather than reaching into `future` + /// itself, so the locking is stated once. + async fn run(computation: Arc>) -> T { + let mut fut = computation.future.lock().await; + fut.as_mut().await + } + + /// The `async` builder value. + /// + /// Fable resolves `DefaultAsyncBuilder` to this and emits every builder + /// method it does not map to a module function -- Combine, While, For, + /// TryWith, TryFinally, Using -- as an instance call on it. Without the + /// value and the methods, any async block containing a computation in + /// statement position failed to compile. + pub struct AsyncBuilder {} + + pub static singleton: AsyncBuilder = AsyncBuilder {}; + + impl AsyncBuilder { + /// Two computations in sequence, discarding the first result. This is + /// what `if cond then do! x` followed by anything else compiles to. + pub fn Combine( + &self, + computation1: Arc>, + computation2: Arc>, + ) -> Arc> { + Async::from_future(async move { + run(computation1).await; + run(computation2).await + }) + } + + /// `while` inside an async block. + /// + /// The body has to run once per iteration, and a future can only be + /// polled to completion once, so each lap asks `restart` for a fresh + /// one. The F# compiler always wraps a loop body in `Delay`, which is + /// what sets it. + pub fn While(&self, guard: Func0, computation: Arc>) -> Arc> { + Async::from_future(async move { + let mut lap = 0usize; + + while guard() { + let body = match &computation.restart { + Some(build) => build(), + // Nothing to rebuild from. One lap can still be served + // by the computation itself; a second would poll a + // future that has already completed, so say so rather + // than panicking from inside the future. + None if lap == 0 => computation.clone(), + None => panic!( + "async while: the loop body cannot be run more than once" + ), + }; + + run(body).await; + lap += 1; + } + }) + } + + /// `for` inside an async block. + /// + /// The sequence is materialised before anything is awaited, the same + /// way `Async.Parallel` does it: a Fable `Seq`'s iterator is not `Send` + /// and so cannot be held across an await. A lazy, side-effecting + /// sequence therefore runs to completion up front instead of + /// interleaving with the body. + pub fn For( + &self, + sequence: Seq, + body: Func1>>, + ) -> Arc> { + let items: Vec = crate::Seq_::toArray(sequence).iter().cloned().collect(); + + Async::from_future(async move { + for item in items { + run(body(item)).await; + } + }) + } + + /// `try/with` inside an async block. + /// + /// Exceptions are panics on this target, and the body spans await + /// points, so the catch has to wrap the whole computation rather than a + /// synchronous call the way `Exception_::try_catch` does. That also + /// means the panic hook is left alone: it is process-global, and + /// swapping it for the duration of an await would silence unrelated + /// panics on other threads. A caught exception therefore still prints + /// the default panic message. + pub fn TryWith( + &self, + computation: Arc>, + catchHandler: Func1, Arc>>, + ) -> Arc> { + Async::from_future(async move { + // The panic payload is only Send, never Sync, so it must not + // still be alive at the await below -- that would make this + // future non-Sync and `Async` requires Sync. Turning it into + // the handler's computation here ends its scope first. + let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { + Ok(value) => Ok(value), + Err(payload) => Err(catchHandler(get_exception(&*payload))), + }; + + match outcome { + Ok(value) => value, + Err(handled) => run(handled).await, + } + }) + } + + /// `try/finally` inside an async block. The compensation runs on both + /// paths, and a failure keeps unwinding with its original payload. + pub fn TryFinally( + &self, + computation: Arc>, + compensation: Func0<()>, + ) -> Arc> { + Async::from_future(async move { + let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; + compensation(); + + match outcome { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } + }) + } + + /// `use` inside an async block. + /// + /// Deliberately does not call Dispose. A plain `use` does not call it + /// on this target either -- resources go out with Rust's own Drop, see + /// the disabled cases in `tests/Rust/tests/src/MiscTests.fs` -- and + /// disposing here alone would make the async and non-async forms + /// disagree. This exists so `use` inside `async` compiles and binds, + /// which it previously did not. + pub fn Using< + D: Clone + Send + Sync + 'static, + U: Clone + Send + Sync + 'static, + >( + &self, + resource: D, + binder: Func1>>, + ) -> Arc> { + binder(resource) + } + } + + /// `Delay` is what makes an async block a description rather than a running + /// computation, so the builder is called when the result is awaited and not + /// before. + /// + /// The builder is taken as `EraseUnitArg` rather than plainly as `Func0` + /// because the unit parameter of a `unit -> Async` lambda is not + /// always erased: `discardUnitArg` keeps it when the lambda's own generic + /// argument is unit, which is what a `try/finally` nested inside a + /// `try/with` produces. Accepting both shapes and normalising here is + /// contained to this file; changing when the compiler erases a unit + /// parameter would reach every lambda on the target. + /// + /// It used to be called immediately, which meant any part of the block + /// evaluated eagerly -- `return i` compiles to `r_return(i)`, and `r_return` + /// takes its argument by value -- captured state from before the block ran. + /// Nothing noticed while every async block was a straight line, because the + /// value was read at construction and the block did nothing in between. + /// `Combine` makes it visible: in `while .. done; return i` the `return i` + /// half was read before the loop had touched `i`. + pub fn delay>>>( + binder: F, + ) -> Arc> { + let binder = eraseUnitArg(binder); + let build = binder.clone(); - pub fn delay(binder: Func0>>) -> Arc> { - let pr = binder(); Arc::from(Async { - future: pr.future.clone(), + future: Arc::from(Mutex::from(Box::pin(async move { run(build()).await }) + as Pin + Send + Sync + 'static>>)), + // The only place F# gives us a way to build the computation again. + // While and For need one; nothing else looks at it. + restart: Some(binder), }) } @@ -115,18 +348,11 @@ pub mod AsyncBuilder_ { next.as_mut().await }; - let b: Pin + Send + Sync + 'static>> = Box::pin(next); - Arc::from(Async { - future: Arc::from(Mutex::from(b)), - }) + Async::from_future(next) } pub fn r_return(item: T) -> Arc> { - let r = ready(item); - let b: Pin + Send + Sync + 'static>> = Box::pin(r); - Arc::from(Async { - future: Arc::from(Mutex::from(b)), - }) + Async::from_future(ready(item)) } pub fn return_from(computation: Arc>) -> Arc> { @@ -202,8 +428,8 @@ pub mod Monitor_ { #[cfg(feature = "threaded")] pub mod Task_ { use std::pin::Pin; - use std::sync::{Arc, RwLock}; - use std::task::Poll; + use std::sync::{Arc, Mutex, RwLock}; + use std::task::{Poll, Waker}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -255,6 +481,10 @@ pub mod Task_ { #[derive(Clone)] pub struct Task { result: Arc>>, + // Whoever is awaiting this task. Polling used to sleep the thread for + // 100ms and try again, which made awaiting a task cost far more than + // the work it was waiting for. + wakers: Arc>>, } impl Future for &Task { @@ -264,29 +494,25 @@ pub mod Task_ { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - //eprintln!("{:?} Polling task for result", thread::current().id()); - let m = self.result.read().unwrap(); + if let TaskState::Complete(res) = &*self.result.read().unwrap() { + return Poll::Ready(res.clone()); + } - match &*m { - TaskState::New(_) => { - //schedule? - //eprintln!("{:?} poll: task is new, waking up", thread::current().id()); - cx.waker().wake_by_ref(); - Poll::Pending - } - TaskState::Running => { - //eprintln!("{:?} poll: pending, nothing to do", thread::current().id()); + // Register before re-reading the state: set_result may complete the + // task between the check above and this push, and the re-read below + // is what stops that racing completion from being missed. + self.wakers.lock().unwrap().push(cx.waker().clone()); - //todo - this is no good as it blocks the thread. It needs to be non-blocking and delegate out - thread::sleep(Duration::from_millis(100)); + match &*self.result.read().unwrap() { + TaskState::Complete(res) => Poll::Ready(res.clone()), + // Not started yet. start() flips New to Running synchronously + // before spawning, so this window is short. + TaskState::New(_) => { cx.waker().wake_by_ref(); - Poll::Pending } - TaskState::Complete(res) => { - //eprintln!("{:?} Poll succeeded", thread::current().id()); - Poll::Ready(res.clone()) - } + // Running: set_result wakes us. + TaskState::Running => Poll::Pending, } } } @@ -295,20 +521,30 @@ pub mod Task_ { pub fn new(fut: impl Future + Send + Sync + 'static) -> Task { Task { result: Arc::from(RwLock::from(TaskState::New(Box::pin(fut)))), + wakers: Arc::from(Mutex::from(Vec::new())), } } pub fn from_result(value: T) -> Task { Task { result: Arc::from(RwLock::from(TaskState::Complete(value))), + wakers: Arc::from(Mutex::from(Vec::new())), } } pub fn set_result(&self, value: T) { - let mut m = self.result.write().unwrap(); - //eprintln!("{:?} set task result", thread::current().id()); - (*m) = TaskState::Complete(value); - //eprintln!("{:?} set task result completed", thread::current().id()); + { + let mut m = self.result.write().unwrap(); + (*m) = TaskState::Complete(value); + } + + // Drain first, then wake outside the lock: a waker may poll straight + // back into this task. + let wakers: Vec = self.wakers.lock().unwrap().drain(..).collect(); + + for w in wakers { + w.wake(); + } } pub fn is_new(&self) -> bool { @@ -320,13 +556,12 @@ pub mod Task_ { } pub fn get_result(&self) -> T { - while !self.is_complete() { - //eprintln!("{:?} try get result", thread::current().id()); - thread::sleep(Duration::from_millis(10)); + if let TaskState::Complete(res) = &*self.result.read().unwrap() { + return res.clone(); } - //eprintln!("{:?} has result", thread::current().id()); - let t = self.result.read().unwrap().unwrap(); - t + + // Parks on the waker rather than waking every 10ms to look again. + futures::executor::block_on(self) } pub fn start(t: Arc>) { @@ -387,6 +622,7 @@ pub mod Task_ { pub fn from_result(t: T) -> Arc> { let t = Task { result: Arc::from(RwLock::from(TaskState::Complete(t))), + wakers: Arc::from(Mutex::from(Vec::new())), }; Arc::from(t) } diff --git a/src/fable-library-rust/src/Exception.rs b/src/fable-library-rust/src/Exception.rs index e9a35df832..ce1809f83a 100644 --- a/src/fable-library-rust/src/Exception.rs +++ b/src/fable-library-rust/src/Exception.rs @@ -13,31 +13,37 @@ pub mod Exception_ { try_f() // no catching when no_std } + /// Turns a caught panic payload into the exception a `with` handler sees. + /// + /// Taken by reference so a caller that has to re-raise (the async builder's + /// TryFinally) still owns the payload to resume unwinding with. + #[cfg(not(feature = "no_std"))] + pub fn get_exception(err: &dyn Any) -> LrcPtr { + match err.downcast_ref::<&'static str>() { + Some(s) => new_Exception(string(*s)), + None => match err.downcast_ref::() { + Some(s) => new_Exception(fromSlice(s)), + None => match err.downcast_ref::>() { + Some(ex) => ex.clone(), + None => new_Exception(string("Unknown error")), + }, + }, + } + } + #[cfg(not(feature = "no_std"))] pub fn try_catch(try_f: F, catch_f: G) -> R where F: FnOnce() -> R, G: FnOnce(LrcPtr) -> R, { - fn get_ex(err: Box) -> LrcPtr { - match err.downcast_ref::<&'static str>() { - Some(s) => new_Exception(string(*s)), - None => match err.downcast_ref::() { - Some(s) => new_Exception(fromSlice(s)), - None => match err.downcast_ref::>() { - Some(ex) => ex.clone(), - None => new_Exception(string("Unknown error")), - }, - }, - } - } let prev_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(|_| {})); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(try_f)); std::panic::set_hook(prev_hook); match result { Ok(res) => res, - Err(err) => catch_f(get_ex(err)), + Err(err) => catch_f(get_exception(&*err)), } } diff --git a/tests/Rust/tests/src/AsyncTests.fs b/tests/Rust/tests/src/AsyncTests.fs index 8cfb295020..b617f49823 100644 --- a/tests/Rust/tests/src/AsyncTests.fs +++ b/tests/Rust/tests/src/AsyncTests.fs @@ -82,13 +82,33 @@ let ``return! propagates error Result from inner async`` () = Async.RunSynchronously (outer (Ok 99)) |> equal (Ok 99) Async.RunSynchronously (outer (Error "oops")) |> equal (Error "oops") -// [] -// let shouldExecAsParallelStructurallyCorrect () = -// let t = Async.Parallel [ -// async { return 1 } -// async { return 2 } -// ] -// t |> Async.RunSynchronously |> Array.sum |> equal 3 +[] +let shouldExecAsParallelStructurallyCorrect () = + let t = Async.Parallel [ + async { return 1 } + async { return 2 } + ] + t |> Async.RunSynchronously |> Array.sum |> equal 3 + +[] +let shouldExecAsParallelInOrder () = + Async.Parallel [ + async { return 1 } + async { return 2 } + async { return 3 } + ] + |> Async.RunSynchronously + |> Array.toList + |> equal [ 1; 2; 3 ] + +[] +let shouldExecStartImmediateAsTask () = + // Runs on the calling thread instead of queueing onto the thread pool. + async { return 7 } + |> Async.StartImmediateAsTask + |> Async.AwaitTask + |> Async.RunSynchronously + |> equal 7 // [] // let shouldMarshalMutOverAsyncClosureCorrectly () = @@ -934,3 +954,188 @@ let ``Async.Sleep works`` () = // acc > 2 |> equal true // } // |> Async.StartImmediate + +// The async builder's Combine, While, For, TryWith, TryFinally and Using used +// to be missing from the Rust runtime, so any async block with a computation in +// statement position failed to compile with "unresolved import +// AsyncBuilder_::singleton". + +[] +let ``if without else in an async block combines`` () = + let comp = async { + if 1 < 2 then + do! async { return () } + return 42 + } + comp |> Async.RunSynchronously |> equal 42 + +[] +let ``while in an async block loops`` () = + let comp = async { + let mutable i = 0 + while i < 3 do + let! step = async { return 1 } + i <- i + step + return i + } + comp |> Async.RunSynchronously |> equal 3 + +[] +let ``while in an async block can run zero times`` () = + let comp = async { + let mutable i = 10 + while i < 3 do + let! step = async { return 1 } + i <- i + step + return i + } + comp |> Async.RunSynchronously |> equal 10 + +[] +let ``for in an async block iterates`` () = + let comp = async { + let mutable sum = 0 + for x in [1; 2; 3] do + let! y = async { return x * 10 } + sum <- sum + y + return sum + } + comp |> Async.RunSynchronously |> equal 60 + +// Delay used to run its builder immediately, so the `return` half of the block +// was built -- and its value read -- before the loop above it had run. Only +// Combine makes that visible, which is why it went unnoticed. +[] +let ``state written by a loop is visible after it`` () = + let comp = async { + let mutable acc = "" + for x in ["a"; "b"] do + let! y = async { return x } + acc <- acc + y + return acc + } + comp |> Async.RunSynchronously |> equal "ab" + +[] +let ``try with in an async block catches`` () = + let comp = async { + try + let! _ = async { return 1 } + failwith "boom" + return 0 + with _ -> + return -1 + } + comp |> Async.RunSynchronously |> equal -1 + +[] +let ``try with in an async block leaves a success alone`` () = + let comp = async { + try + let! x = async { return 1 } + return x + with _ -> + return -1 + } + comp |> Async.RunSynchronously |> equal 1 + +[] +let ``try finally in an async block runs the finalizer`` () = + let mutable ran = 0 + let comp = async { + try + let! x = async { return 1 } + return x + finally + ran <- ran + 1 + } + comp |> Async.RunSynchronously |> equal 1 + ran |> equal 1 + +[] +let ``try finally in an async block runs the finalizer on failure`` () = + let mutable ran = 0 + let comp = async { + try + let! _ = async { return 1 } + failwith "boom" + return 0 + finally + ran <- ran + 1 + } + throwsAnyError (fun () -> comp |> Async.RunSynchronously) + ran |> equal 1 + +// Note this asserts only that the resource binds and the block returns. +// Dispose is not called for a `use` on the Rust target at all -- see the +// disabled cases in MiscTests.fs -- so asserting disposal here would pass on +// .NET and fail on Rust. +type AsyncDisposable() = + interface System.IDisposable with + member _.Dispose() = () + +[] +let ``use in an async block binds the resource`` () = + let comp = async { + use res = new AsyncDisposable() + let! x = async { return 1 } + return (if isNull (box res) then 0 else x) + } + comp |> Async.RunSynchronously |> equal 1 + +[] +let ``nested loops in an async block`` () = + let comp = async { + let mutable total = 0 + for i in 1 .. 3 do + let mutable j = 0 + while j < 2 do + let! v = async { return i } + total <- total + v + j <- j + 1 + return total + } + comp |> Async.RunSynchronously |> equal 12 + +[] +let ``a loop body that awaits nothing still runs`` () = + let comp = async { + let mutable i = 0 + while i < 3 do + i <- i + 1 + return i + } + comp |> Async.RunSynchronously |> equal 3 + +[] +let ``try with inside a loop catches each time`` () = + let comp = async { + let mutable caught = 0 + for _ in 1 .. 3 do + try + let! _ = async { return () } + failwith "boom" + with _ -> + caught <- caught + 1 + return caught + } + comp |> Async.RunSynchronously |> equal 3 + +// The inner try/finally makes F# pass Delay a `unit -> Async` lambda whose +// unit parameter is not erased, so the builder arrived as Func1 rather than +// Func0 and this shape did not compile. +[] +let ``try finally nested inside try with`` () = + let comp = async { + let mutable order = "" + try + try + let! _ = async { return () } + failwith "boom" + finally + order <- order + "inner" + with _ -> + order <- order + "-caught" + return order + } + comp |> Async.RunSynchronously |> equal "inner-caught" From 28da1d764e63c39b96ca20e94d9a08786b3c9052 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sun, 30 Aug 2026 12:45:50 +0000 Subject: [PATCH 014/322] fix(rust): F# exceptions, non-exhaustive matches and obj type tests (#4917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Слава Україні! Героям слава! <777696+ncave@users.noreply.github.com> --- src/Fable.Transforms/Rust/Fable2Rust.fs | 71 ++++++++++---- .../src/Fable.Library.Rust.fsproj | 1 + .../src/Microsoft.FSharp.Core.fs | 12 +++ src/fable-library-rust/src/Native.rs | 9 +- tests/Rust/tests/src/ControlFlowTests.fs | 93 +++++++++++++++++++ tests/Rust/tests/src/TypeTests.fs | 36 ++++++- 6 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 src/fable-library-rust/src/Microsoft.FSharp.Core.fs diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index a786a03344..10134dafbf 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -2003,19 +2003,27 @@ module Util = // unsized `dyn Any`, which does not compile). makeLibCall com ctx None "Native" "getZeroObj" [] | _ -> - // Only concrete (sized) `Lrc`-wrapped reference types can use the null-ref - // placeholder. Unsized refs (interfaces, enumerators) and Arc/Box-wrapped - // types keep `getZero` (they don't reach this path in practice). - let isConcreteLrcRef = - match shouldBeRefCountWrapped com ctx typ with - | Some Lrc -> - match typ with - | Fable.DeclaredType(entRef, _) -> not (com.GetEntity(entRef)).IsInterface - | Replacements.Util.IsEnumerator _ -> false - | _ -> true - | _ -> false + // Everything with a `NullableRef` implementation takes the null-ref + // placeholder: concrete (sized) `Lrc`-wrapped reference types, plus + // string, option, array and list, whose library types implement it + // directly. What is left is the primitives, for which `mem::zeroed` + // is a valid bit pattern. + let isNullableRef = + match typ with + | Fable.String + | Fable.Option _ + | Fable.Array _ + | Fable.List _ -> true + | _ -> + match shouldBeRefCountWrapped com ctx typ with + | Some Lrc -> + match typ with + | Fable.DeclaredType(entRef, _) -> not (com.GetEntity(entRef)).IsInterface + | Replacements.Util.IsEnumerator _ -> false + | _ -> true + | _ -> false - if isConcreteLrcRef then + if isNullableRef then makeNull com ctx typ else let genArgsOpt = transformGenArgs com ctx [ typ ] @@ -2107,6 +2115,23 @@ module Util = let ent = com.GetEntity(entRef) let idents = getEntityFieldsAsIdents com ent + // An F# `exception` declaration gets a synthetic __base__ field, so that + // the generated Deref to System.Exception (and hence .Message) works. + // A class constructor puts a matching value at the head of its NewRecord, + // but an exception constructor carries only the declared fields, leaving + // one more ident than values. Supply the base here instead of zipping + // lists of different lengths, which failed with no source location. + let baseFields, idents = + match idents with + | baseIdent :: restIdents when + ent.IsFSharpExceptionDeclaration && List.length restIdents = List.length values + -> + let msg = mkStrLitExpr "" |> makeStaticString com ctx + let baseExpr = makeLibCall com ctx None "Util" "new_Exception" [ msg ] + let fieldName = baseIdent.Name |> sanitizeMember + [ mkExprField [] fieldName baseExpr false false ], restIdents + | _ -> [], idents + let fields = List.zip idents values |> List.map (fun (ident, value) -> @@ -2132,7 +2157,7 @@ module Util = mkExprField [] fieldName expr false false ) - let fields = List.append fields phantomFields + let fields = List.append baseFields (List.append fields phantomFields) let genArgsOpt = transformGenArgs com ctx genArgs let entName = getEntityFullName com ctx entRef @@ -3816,15 +3841,19 @@ module Util = | _, Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _) -> Some(left, right) | Fable.Value((Fable.CharConstant _ | Fable.NumberConstant _), _), _ -> Some(right, left) | _ -> None - // Only a plain-ident scrutinee is convertible: transformSwitch can't recover the - // `Option` type from anything else, and would emit bogus `0_i32` patterns. + // Only a plain-ident scrutinee is convertible: transformSwitch recovers the + // union (or `Option`) type from the ident, and without one it falls back to + // `evalExpr.Type`, which is the tag's `int32`. `makeUnionCasePatOpt` then + // declines and the arm becomes a bare `1_i32` literal pattern matched + // against the union value itself, which does not compile. Anything else is + // left to the if/else decision-tree path, which tests with `if let`. | Fable.Test(Fable.IdentExpr _ as expr, Fable.OptionTest isSome, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) let right = makeIntConst (makeTest isSome 0 1) Some(evalExpr, right) - | Fable.Test(expr, Fable.UnionCaseTest tag, r) -> + | Fable.Test(Fable.IdentExpr _ as expr, Fable.UnionCaseTest tag, r) -> let evalExpr = Fable.Get(expr, Fable.UnionTag, Fable.Number(Int32, Fable.NumberInfo.Empty), r) @@ -5078,7 +5107,11 @@ module Util = let entName = Fable.Naming.splitLast ent.FullName let genArgs = FSharp2Fable.Util.getEntityGenArgs ent let generics = makeGenerics com ctx genArgs - let isPublic = ent.IsFSharpRecord + // F# exception fields (Data0, Data1, ...) are public on .NET, and an + // exception declared in one crate has to be constructible from another — + // MatchFailureException lives in the runtime library but is constructed + // by user code for any match F# cannot prove exhaustive. + let isPublic = ent.IsFSharpRecord || ent.IsFSharpExceptionDeclaration let idents = getEntityFieldsAsIdents com ent let fields = @@ -5307,6 +5340,10 @@ module Util = let fieldValues = getEntityFieldsAsIdents com ent + // Skip the synthetic __base__ field. The message describes the + // exception's own data; formatting the base System.Exception + // with {:?} also walks its null innerException, which faults. + |> List.filter (fun ident -> ident.Name <> baseName) |> List.map (fun ident -> let info = Fable.FieldInfo.Create(ident.Name, ident.Type, ident.IsMutable) Fable.Get(thisArg, info, ident.Type, None) diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 30ccc1242e..4b2c06b55f 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/fable-library-rust/src/Microsoft.FSharp.Core.fs b/src/fable-library-rust/src/Microsoft.FSharp.Core.fs new file mode 100644 index 0000000000..6d7f12d28a --- /dev/null +++ b/src/fable-library-rust/src/Microsoft.FSharp.Core.fs @@ -0,0 +1,12 @@ +namespace Microsoft.FSharp.Core + +// F# emits `raise (MatchFailureException(file, line, col))` for any match it +// cannot prove exhaustive. The other targets import this type from their runtime +// library (Replacements.fs maps Types.matchFail to it); Rust had no definition, +// so such a match referenced a type that did not exist and the generated crate +// failed with `unresolved import ...Microsoft::FSharp::Core::MatchFailureException`. +// +// Kept in its own file rather than appended to System.fs: a second namespace in +// that file makes the generated module glob-import System's own Exception +// alongside its definition, which Rust rejects as a duplicate name. +exception MatchFailureException of string * int * int diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index c16859d49c..6c2a627fa3 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -492,11 +492,14 @@ pub mod Native_ { // Type testing // ----------------------------------------------------------- - pub fn try_downcast(value: &T) -> Option<&U> { + // Returns an owned value rather than a reference: `match o with :? string as s` + // binds `s` as the tested type, and codegen emits `if let Some(s) = try_downcast(..)`, + // so a borrow here made `s` a `&U` wherever a `U` was expected. + pub fn try_downcast(value: &T) -> Option { if let Some(o) = (value as &dyn Any).downcast_ref::>() { - o.downcast_ref::() + o.downcast_ref::().cloned() } else { - (value as &dyn Any).downcast_ref::() + (value as &dyn Any).downcast_ref::().cloned() } } diff --git a/tests/Rust/tests/src/ControlFlowTests.fs b/tests/Rust/tests/src/ControlFlowTests.fs index 66e345ced8..b44f1ed7ff 100644 --- a/tests/Rust/tests/src/ControlFlowTests.fs +++ b/tests/Rust/tests/src/ControlFlowTests.fs @@ -184,6 +184,99 @@ let ``two-switch or-pattern with obj binding works`` () = orPatObj (OA(box 9)) :? int |> equal true orPatObj (OB(box 11)) :? string |> equal false +// An active pattern in the first clause of a match F# cannot prove exhaustive. +// F# emits `raise (MatchFailureException(...))` for the fallthrough, and the +// rewrite that normally removes it only fires when the decision expression +// starts with a UnionCaseTest — an active pattern there is not one, so the +// construction survived into codegen and crashed the compiler with +// "The lists had different lengths" and no source location. +type private ApExpr = + | ApColumn of string * string + | ApConst of string + +let private (|IsApConst|_|) e = + match e with + | ApConst c -> Some c + | _ -> None + +let private (|IsApColumn|_|) e = + match e with + | ApColumn(t, n) -> Some(t, n) + | _ -> None + +let private renderUnionAfterAp e = + match e with + | IsApConst c -> c + | ApColumn(t, n) -> t + "." + n + +let private renderApOnly e = + match e with + | IsApConst c -> c + | IsApColumn(t, n) -> t + "." + n + +[] +let ``non-exhaustive match starting with an active pattern works`` () = + renderUnionAfterAp (ApColumn("c", "x")) |> equal "c.x" + renderUnionAfterAp (ApConst "lit") |> equal "lit" + renderApOnly (ApColumn("c", "x")) |> equal "c.x" + renderApOnly (ApConst "lit") |> equal "lit" + +// A fieldless union reached through a record field rather than a plain ident. +// The switch conversion recovers the union type from the ident; without one it +// used to fall back to the tag's int32 and emit a bare integer literal pattern +// against the union value, which does not compile. +type JoinKind = + | InnerJoin + | LeftJoin + +type Join = { Kind: JoinKind; Table: string } + +let private renderJoinKind (j: Join) = + let mutable out = "" + + match j.Kind with + | InnerJoin -> out <- "INNER" + | LeftJoin -> out <- "LEFT" + + out + +[] +let ``fieldless union matched through a record field works`` () = + renderJoinKind { Kind = InnerJoin; Table = "t" } |> equal "INNER" + renderJoinKind { Kind = LeftJoin; Table = "t" } |> equal "LEFT" + +// A guard on a nested pattern makes the whole match take the decision-tree +// path, which pre-declares a mutable binding for every variable in the match +// and initialises it before testing anything. Strings, options, arrays and +// lists are heap-backed, and zero-initialising one is a runtime panic rather +// than a compile error. +type Payload = + | Number of int + | Numbers of int[] + | Names of string list + | Maybe of int option + | Label of string + +let private describePayload (p: Payload) (flag: bool) = + match p with + | Maybe(Some v) when flag -> "guarded " + string v + | Maybe(Some v) -> "some " + string v + | Maybe None -> "none" + | Number n -> "number " + string n + | Numbers xs -> "numbers " + string xs.Length + | Names ns -> "names " + string (List.length ns) + | Label s -> "label " + s + +[] +let ``a guarded nested pattern does not zero-initialise the other bindings`` () = + describePayload (Maybe(Some 1)) true |> equal "guarded 1" + describePayload (Maybe(Some 2)) false |> equal "some 2" + describePayload (Maybe None) false |> equal "none" + describePayload (Number 3) false |> equal "number 3" + describePayload (Numbers [| 1; 2; 3 |]) false |> equal "numbers 3" + describePayload (Names [ "a"; "b" ]) false |> equal "names 2" + describePayload (Label "hi") false |> equal "label hi" + // A try/with/finally lowers to closures on this target, but the Fable AST has no // Lambda there, so the capture walk did not treat those bodies as closure // contexts. A `let mutable` assigned in a finally block was emitted as a bare diff --git a/tests/Rust/tests/src/TypeTests.fs b/tests/Rust/tests/src/TypeTests.fs index 752315a872..cde8f668e6 100644 --- a/tests/Rust/tests/src/TypeTests.fs +++ b/tests/Rust/tests/src/TypeTests.fs @@ -296,7 +296,7 @@ type Point2D = new(xy: float) = { X = xy; Y = xy } end -// exception MyEx of int*string +exception MyEx of int * string // type MyEx2(f: float) = // inherit exn(sprintf "Code: %i" (int f)) @@ -1417,3 +1417,37 @@ let ``ArgumentException with message and inner exception works`` () = let ``Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true + +// Constructing an F# exception value used to crash the compiler outright: +// getEntityFieldsAsIdents prepends a synthetic __base__ field, and makeRecord +// zipped it against a value list that has no base value. Raising and catching +// one by its pattern is a separate matter and still unsupported here, so this +// stays on the construct/destructure path that a fixed makeRecord covers. +[] +let ``F# exception values can be constructed and destructured`` () = + let e = MyEx(4, "ERROR") + + match e with + | MyEx(code, msg) -> + code |> equal 4 + msg |> equal "ERROR" + | _ -> failwith "expected MyEx" + +// `match (o: obj) with :? string as s` binds s as the tested type, and codegen +// emits `if let Some(s) = try_downcast(..)`. try_downcast returned a reference, +// so s came out as &T wherever a T was expected and the crate did not compile. +let private describeObj (o: obj) = + match o with + | :? string as s -> "'" + s + "'" + | :? int as i -> string i + | :? float as f -> string f + | :? bool as b -> (if b then "1" else "0") + | _ -> "?" + +[] +let ``type test binding on obj yields a usable value`` () = + describeObj (box "a") |> equal "'a'" + describeObj (box 7) |> equal "7" + describeObj (box 2.5) |> equal "2.5" + describeObj (box true) |> equal "1" + describeObj (box 'c') |> equal "?" From 2fb535a7cd35cef501c56684f205e2dffa3c36e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:55:07 -0700 Subject: [PATCH 015/322] fix(all): Emit instance calls for virtual methods inherited from Object (#4926) --- src/Fable.Transforms/Beam/Replacements.fs | 19 +++++++++++------- src/Fable.Transforms/Dart/Replacements.fs | 19 +++++++++++------- src/Fable.Transforms/Python/Replacements.fs | 19 +++++++++++------- src/Fable.Transforms/Replacements.fs | 19 +++++++++++------- src/fable-library-beam/System.Text.fs | 12 +++++------ src/fable-library-dart/System.Text.fs | 20 +++++++++---------- .../fable_library/System.Text.fs | 20 +++++++++---------- src/fable-library-rust/src/System.Text.fs | 14 ++++++++----- src/fable-library-ts/System.Text.fs | 20 +++++++++---------- 9 files changed, 93 insertions(+), 69 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 33738383c2..a2a6a1bd35 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5395,15 +5395,20 @@ let private getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let private bclType (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let tryType (_t: Type) = None diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 3b25868286..f9f0a6dcaa 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -854,15 +854,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, "toString", t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 997d42d43d..6cd219feac 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1072,15 +1072,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 769a1dba58..c553e134a5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1272,15 +1272,20 @@ let getMangledNames (i: CallInfo) (thisArg: Expr option) = moduleName, mangledName let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName, mangledName = getMangledNames i thisArg + match i.CompiledName, thisArg, args with + | "ToString", Some callee, [] -> Helper.InstanceCall(callee, "toString", t, args, ?loc = r) |> Some + | "GetHashCode", Some callee, [] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | "Equals", Some callee, [ other ] -> Helper.InstanceCall(callee, i.CompiledName, t, args, ?loc = r) |> Some + | _ -> + let moduleName, mangledName = getMangledNames i thisArg - let args = - match thisArg with - | Some callee -> callee :: args - | _ -> args + let args = + match thisArg with + | Some callee -> callee :: args + | _ -> args - Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) - |> Some + Helper.LibCall(com, moduleName, mangledName, t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, mangledName = getMangledNames i thisArg diff --git a/src/fable-library-beam/System.Text.fs b/src/fable-library-beam/System.Text.fs index ad653f440a..fe5b5d52b3 100644 --- a/src/fable-library-beam/System.Text.fs +++ b/src/fable-library-beam/System.Text.fs @@ -71,6 +71,10 @@ type StringBuilder(value: string, capacity: int) = buf.Clear() x + member x.get_Chars(index: int) : char = + let str = x.ToString() + str.[index] + member x.Length = let mutable len = 0 @@ -89,12 +93,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.get_Chars(index: int) : char = - let str = x.ToString() - str.[index] - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) diff --git a/src/fable-library-dart/System.Text.fs b/src/fable-library-dart/System.Text.fs index 44c09f4b1a..0c2bc3a593 100644 --- a/src/fable-library-dart/System.Text.fs +++ b/src/fable-library-dart/System.Text.fs @@ -127,6 +127,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = let oldValue = string oldValue let newValue = string newValue @@ -140,16 +148,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) diff --git a/src/fable-library-py/fable_library/System.Text.fs b/src/fable-library-py/fable_library/System.Text.fs index debe8a2cf1..ee4c44a886 100644 --- a/src/fable-library-py/fable_library/System.Text.fs +++ b/src/fable-library-py/fable_library/System.Text.fs @@ -162,6 +162,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = for i = buf.Count - 1 downto 0 do buf[i] <- buf[i].Replace(oldValue, newValue) @@ -172,19 +180,11 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) // ------------------------------------------------------------------------- // Object overrides diff --git a/src/fable-library-rust/src/System.Text.fs b/src/fable-library-rust/src/System.Text.fs index 0735dde387..3a57ee61a7 100644 --- a/src/fable-library-rust/src/System.Text.fs +++ b/src/fable-library-rust/src/System.Text.fs @@ -13,8 +13,12 @@ type StringBuilder(value: string, capacity: int) = new(value: string) = StringBuilder(value, 16) new() = StringBuilder("", 16) - member x.Append(s: string) = - buf.Add(s) + member x.Append(s: string | null) = + buf.Add(string s) + x + + member x.Append(s: string | null, startIndex: int, count: int) = + buf.Add((string s).Substring(startIndex, count)) x member x.Append(o: bool) = x.Append(string o) @@ -36,8 +40,6 @@ type StringBuilder(value: string, capacity: int) = member x.Append(o: float32) = x.Append(string o) member x.Append(o: float) = x.Append(string o) - member x.Append(s: string, index: int, count: int) = x.Append(s.Substring(index, count)) - member x.Append(cs: char[]) = x.Append(System.String(cs)) member x.Append(sb: StringBuilder) = x.Append(sb.ToString()) // member x.Append(o: obj) = x.Append(string o) @@ -106,4 +108,6 @@ type StringBuilder(value: string, capacity: int) = override _.ToString() = System.String.Concat(buf |> asArray) - member x.ToString(index: int, count: int) = x.ToString().Substring(index, count) + member x.ToString(startIndex: int, length: int) = + let str = x.ToString() + str.Substring(startIndex, length) diff --git a/src/fable-library-ts/System.Text.fs b/src/fable-library-ts/System.Text.fs index 685cc1df3d..e43977bb5e 100644 --- a/src/fable-library-ts/System.Text.fs +++ b/src/fable-library-ts/System.Text.fs @@ -159,6 +159,14 @@ type StringBuilder(value: string, capacity: int) = let pos = index - len buf[i] <- buf[i][0 .. (pos - 1)] + (string value) + buf[i][(pos + 1) ..] + member x.Length = + let mutable len = 0 + + for i = buf.Count - 1 downto 0 do + len <- len + buf[i].Length + + len + member x.Replace(oldValue: char, newValue: char) = for i = buf.Count - 1 downto 0 do buf[i] <- buf[i].Replace(oldValue, newValue) @@ -169,16 +177,8 @@ type StringBuilder(value: string, capacity: int) = let str = x.ToString().Replace(oldValue, newValue) x.Clear().Append(str) - member x.Length = - let mutable len = 0 - - for i = buf.Count - 1 downto 0 do - len <- len + buf[i].Length - - len - override _.ToString() = System.String.Concat(buf) - member x.ToString(firstIndex: int, length: int) = + member x.ToString(startIndex: int, length: int) = let str = x.ToString() - str.Substring(firstIndex, length) + str.Substring(startIndex, length) From 1289360a11398387712df2769069b9c949659966 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 31 Aug 2026 17:59:12 +0200 Subject: [PATCH 016/322] fix: preserve project FSharp.Core reference (#4924) Co-authored-by: Mangel Maxime --- src/Fable.Compiler/ProjectCracker.fs | 10 +--------- .../data/taskNetstandard/TaskBuilder.fs | 16 ++++++++++++++++ .../taskNetstandard/TaskBuilder.jsx.expected | 13 +++++++++++++ .../data/taskNetstandard/taskNetstandard.fsproj | 15 +++++++++++++++ 4 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs create mode 100644 tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected create mode 100644 tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index 24bc8b08e2..bf2ee2705d 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -1002,7 +1002,6 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) let ignoredRefs = HashSet [ - "FSharp.Core" "WindowsBase" "Microsoft.Win32.Primitives" "Microsoft.VisualBasic" @@ -1027,14 +1026,7 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) let projRefs = projRefs |> List.map (fun p -> p.ProjectFile) - let otherOptions = - [| - yield! otherOptions - yield! dllRefs - // For some reason, in my tests it seems to work without the FSharp.Core reference - // but we add it just in case - yield "-r:" + typeof>.Assembly.Location - |] + let otherOptions = [| yield! otherOptions; yield! dllRefs |] let outputType = match mainProj.OutputType with diff --git a/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs new file mode 100644 index 0000000000..9c41313ad7 --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.fs @@ -0,0 +1,16 @@ +module TaskBuilder + +open System.Threading.Tasks + +let run shouldThrow : Task> = + task { + let! value = Task.FromResult 42 + + try + if shouldThrow then + failwith "boom" + + return Ok value + with error -> + return Error error + } diff --git a/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected new file mode 100644 index 0000000000..33b008a4da --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/TaskBuilder.jsx.expected @@ -0,0 +1,13 @@ + +import { task } from "./fable_modules/fable-library-js.5.15.0/TaskBuilder.js"; +import { fromResult } from "./fable_modules/fable-library-js.5.15.0/Task.js"; +import { Exception } from "./fable_modules/fable-library-js.5.15.0/Util.js"; +import { FSharpResult$2 } from "./fable_modules/fable-library-js.5.15.0/Result.js"; + +export function run(shouldThrow) { + const builder$0040 = task(); + return builder$0040.Run(builder$0040.Delay(() => builder$0040.Bind(fromResult(42), (_arg) => builder$0040.TryWith(builder$0040.Delay(() => builder$0040.Combine(shouldThrow ? (((() => { + throw new Exception("boom"); + })(), builder$0040.Zero())) : builder$0040.Zero(), builder$0040.Delay(() => builder$0040.Return(new FSharpResult$2(/* Ok */ 0, [_arg]))))), (_arg_1) => builder$0040.Return(new FSharpResult$2(/* Error */ 1, [_arg_1])))))); +} + diff --git a/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj b/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj new file mode 100644 index 0000000000..024261748d --- /dev/null +++ b/tests/Integration/Integration/data/taskNetstandard/taskNetstandard.fsproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + From b0740ba95859a80e7add07f21e2fc81a31c00853 Mon Sep 17 00:00:00 2001 From: roboz0r Date: Mon, 31 Aug 2026 15:47:41 -0500 Subject: [PATCH 017/322] fix(all): Allow private inline functions to use private values (#4915) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/FSharp2Fable.Util.fs | 13 +- src/Fable.Transforms/FSharp2Fable.fs | 26 +++ src/Fable.Transforms/Global/Compiler.fs | 1 + src/Fable.Transforms/Transforms.Util.fs | 10 +- .../Compiler/CompilerMessagesTests.fs | 149 ++++++++++++++++++ .../Compiler/TestProject/.gitignore | 3 +- .../Compiler/TestProject/TestProject.fsproj | 1 + .../Compiler/TestProjectSignature/.gitignore | 3 + .../TestProjectSignature.fsproj | 19 +++ tests/Integration/Compiler/Util/Compiler.fs | 105 +++++++----- 10 files changed, 281 insertions(+), 49 deletions(-) create mode 100644 tests/Integration/Compiler/TestProjectSignature/.gitignore create mode 100644 tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 389bd21190..acbd96349e 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -2445,6 +2445,11 @@ module Util = else Some(entityIdent com ent.Ref) + let private isInlinedInDeclaringFileOnly (ctx: Context) = + match ctx.PrecompilingInlineFunction with + | Some inlineMemb -> not (isNotPrivate inlineMemb) + | None -> false + let memberIdent (com: Compiler) (ctx: Context) r typ (memb: FSharpMemberOrFunctionOrValue) membRef = let r = r |> Option.map (fun r -> { r with identifierName = Some memb.DisplayName }) @@ -2495,11 +2500,15 @@ module Util = // Private values are not exported so they can't be imported by call sites in other files. // We need to handle it manually because Fable handle resolve inline function itself - if com.IsPrecompilingInlineFunction && memb.Accessibility.IsPrivate then + if + com.IsPrecompilingInlineFunction + && not (isNotPrivate memb) + && not (isInlinedInDeclaringFileOnly ctx) + then $"The value '%s{memb.DisplayName}' was marked inline but its implementation makes use of an internal or private function which is not sufficiently accessible" |> addError com [] r - makeInternalMemberImport com typ membRef memberName file + makeInternalMemberImport com typ membRef memberName file r let getFunctionMemberRef (memb: FSharpMemberOrFunctionOrValue) = match memb.DeclaringEntity with diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 2e21c18583..ddbabedd80 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2301,6 +2301,7 @@ type InlineExprInfo = FileName: string ScopeIdents: Set ResolvedIdents: Dictionary + IsFileLocal: bool } let resolveInlineIdent (ctx: Context) (info: InlineExprInfo) (ident: Fable.Ident) = @@ -2322,6 +2323,27 @@ let resolveInlineIdent (ctx: Context) (info: InlineExprInfo) (ident: Fable.Ident else ident +let checkInlinedPrivateAccess + (com: IFableCompiler) + (ctx: Context) + (info: InlineExprInfo) + (importInfo: Fable.ImportInfo) + r + = + match importInfo.Kind, info.IsFileLocal with + | Fable.MemberImport membRef, true -> + match com.TryGetMember(membRef) with + | Some memb when not memb.IsPublic -> + let r = + match r, ctx.InlinePath with + | None, { ToRange = toRange } :: _ -> toRange + | _ -> r + + $"The value '%s{memb.DisplayName}' was marked inline but its implementation makes use of an internal or private function which is not sufficiently accessible" + |> addError com ctx.InlinePath r + | _ -> () + | _ -> () + let resolveInlinedCallInfo com (ctx: Context) info (callInfo: Fable.CallInfo) = { callInfo with ThisArg = Option.map (resolveInlineExpr com ctx info) callInfo.ThisArg @@ -2513,6 +2535,8 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = if isImportToSameFile then Fable.IdentExpr { makeTypedIdent t importInfo.Selector with Range = r } else + checkInlinedPrivateAccess com ctx info importInfo r + let path = fixImportedRelativePath com importInfo.Path info.FileName Fable.Import({ importInfo with Path = path }, t, r) @@ -2729,6 +2753,7 @@ type FableCompiler(com: Compiler) = FileName = inExpr.FileName ScopeIdents = inExpr.ScopeIdents ResolvedIdents = Dictionary() + IsFileLocal = inExpr.IsFileLocal } let ctx, bindings, forceInlineMap = @@ -3002,6 +3027,7 @@ let getInlineExprs fileName (declarations: FSharpImplementationFileDeclaration l FileName = fileName GenericArgs = genArgs ScopeIdents = set ctx.UsedNamesInDeclarationScope + IsFileLocal = not (isNotPrivate memb) } ) ] diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 39937961a3..37ba379f99 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -58,6 +58,7 @@ type InlineExpr = FileName: string GenericArgs: string list ScopeIdents: Set + IsFileLocal: bool } type CompilerPlugins = { MemberDeclarationPlugins: Map } diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 22e7f9bbe8..62009fc090 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -1263,7 +1263,7 @@ module AST = LibraryImportInfo.Create(isInstanceMember = false, isModuleMember = true) |> makeImportLibWithInfo com t memberName moduleName - let private makeInternalImport (com: Compiler) t (selector: string) (path: string) kind = + let private makeInternalImport (com: Compiler) t (selector: string) (path: string) range kind = let path = if com.CurrentFile = path then "./" + Path.GetFileName(path) @@ -1277,14 +1277,14 @@ module AST = Kind = kind }, t, - None + range ) - let makeInternalMemberImport com t membRef (selector: string) (path: string) = - MemberImport(membRef) |> makeInternalImport com t selector path + let makeInternalMemberImport com t membRef (selector: string) (path: string) range = + MemberImport(membRef) |> makeInternalImport com t selector path range let makeInternalClassImport com entRef (selector: string) (path: string) = - ClassImport(entRef) |> makeInternalImport com Any selector path + ClassImport(entRef) |> makeInternalImport com Any selector path None let makeCallInfo thisArg args sigArgTypes = CallInfo.Create(?thisArg = thisArg, args = args, sigArgTypes = sigArgTypes) diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 3289c6baff..88dae4532f 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -7,6 +7,12 @@ open Fable.Tests.Compiler.Util.Compiler let private compile source = Compiler.Cached.compile Compiler.Settings.standard source +let private compileWithLibrary library source = + Compiler.Cached.compileWithLibrary Compiler.Settings.standard library source + +let private compileWithSignedLibrary signature library source = + Compiler.Cached.compileWithSignedLibrary Compiler.Settings.standard signature library source + let tests = testList "Compiler Messages" [ testCase "Compile Console.WriteLine" <| fun _ -> @@ -107,6 +113,149 @@ let version = Semver.SemVersion.Parse("1.0.0", 1024) |> Assert.Exists.errorWith "Cannot reference member from .dll reference, Fable packages must include F# sources" |> ignore + testCase "Private inline function referencing private value succeeds" <| fun _ -> + let source = + """ +let private x = 1 +let inline private y () = x +let z = y () +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function referencing private function succeeds" <| fun _ -> + let source = + """ +let private add a b = a + b +let inline private addOne x = add x 1 +let z = addOne 41 +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function in nested module referencing private value succeeds" <| fun _ -> + let source = + """ +module Nested = + let private x = 1 + let inline private y () = x + let z = y () +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Private inline function referencing private value errors when inlined in another file" <| fun _ -> + let library = + """ +module Library + +let private x = 1 +let inline private y () = x +let inline z () = y () +""" + let source = "let res = Library.z ()" + compileWithLibrary library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function referencing private value is reported once, not once per call site" <| fun _ -> + let library = + """ +module Library + +let private x = 1 +let inline w () = x +let inline z () = w () +""" + let source = + """ +let a = Library.z () +let b = Library.z () +""" + compileWithLibrary library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function referencing non-private value from another file succeeds" <| fun _ -> + let library = + """ +module Library + +let x = 1 +let internal w = 2 +let inline private y () = x + w +let inline z () = y () +""" + let source = "let res = Library.z ()" + compileWithLibrary library source + |> Assert.Is.success + |> ignore + + testCase "Inline function referencing value hidden by a signature file errors when inlined in another file" <| fun _ -> + let signature = + """ +module Library + +val inline z: unit -> int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +""" + let source = "let res = Library.z ()" + compileWithSignedLibrary signature library source + |> Assert.Are.errors 1 + |> Assert.Exists.errorWith "was marked inline but its implementation makes use of an internal or private function" + |> ignore + + testCase "Inline function hidden by a signature file referencing hidden value succeeds" <| fun _ -> + let signature = + """ +module Library + +val res: int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +let res = z () +""" + let source = "let res = Library.res" + compileWithSignedLibrary signature library source + |> Assert.Is.success + |> ignore + + testCase "Inline function referencing value exposed by a signature file succeeds" <| fun _ -> + let signature = + """ +module Library + +val x: int +val inline z: unit -> int +""" + let library = + """ +module Library + +let x = 1 +let inline z () = x +""" + let source = "let res = Library.z ()" + compileWithSignedLibrary signature library source + |> Assert.Is.success + |> ignore + testCase "Duplicate attached member names emit a warning" <| fun _ -> let source = """ diff --git a/tests/Integration/Compiler/TestProject/.gitignore b/tests/Integration/Compiler/TestProject/.gitignore index 19c254c5db..437dc5f5ac 100644 --- a/tests/Integration/Compiler/TestProject/.gitignore +++ b/tests/Integration/Compiler/TestProject/.gitignore @@ -1 +1,2 @@ -Program.fs \ No newline at end of file +Program.fs +Library.fs diff --git a/tests/Integration/Compiler/TestProject/TestProject.fsproj b/tests/Integration/Compiler/TestProject/TestProject.fsproj index 1b7d67737c..80a9298133 100644 --- a/tests/Integration/Compiler/TestProject/TestProject.fsproj +++ b/tests/Integration/Compiler/TestProject/TestProject.fsproj @@ -7,6 +7,7 @@ + diff --git a/tests/Integration/Compiler/TestProjectSignature/.gitignore b/tests/Integration/Compiler/TestProjectSignature/.gitignore new file mode 100644 index 0000000000..9baf46c59f --- /dev/null +++ b/tests/Integration/Compiler/TestProjectSignature/.gitignore @@ -0,0 +1,3 @@ +Library.fsi +Library.fs +Program.fs diff --git a/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj b/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj new file mode 100644 index 0000000000..7e8a49ecca --- /dev/null +++ b/tests/Integration/Compiler/TestProjectSignature/TestProjectSignature.fsproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + + + diff --git a/tests/Integration/Compiler/Util/Compiler.fs b/tests/Integration/Compiler/Util/Compiler.fs index 4a6b62ffa7..45ca9148dc 100644 --- a/tests/Integration/Compiler/Util/Compiler.fs +++ b/tests/Integration/Compiler/Util/Compiler.fs @@ -27,36 +27,56 @@ module Compiler = /// NOTE: NOT threadsafe /// -> don't use `parallel` module Cached = - let projDir = IO.Path.Join(__SOURCE_DIRECTORY__, "../TestProject" ) |> Path.normalizeFullPath - let projFile = IO.Path.Join(projDir, "TestProject.fsproj" ) |> Path.normalizeFullPath - let sourceFile = IO.Path.Join(projDir, "Program.fs" ) |> Path.normalizeFullPath - - let cliArgs = - let compilerOptions = CompilerOptionsHelper.Make() - { CliArgs.ProjectFile = projFile - FableLibraryPath = None - RootDir = projDir - Configuration = "Debug" - OutDir = None - IsWatch = false - Precompile = false - PrecompiledLib = None - PrintAst = false - SourceMaps = false - SourceMapsRoot = None - NoRestore = false - NoCache = false - NoGitignore = false - NoParallelTypeCheck = false - Exclude = ["Fable.Core"] - Replace = Map.empty - RunProcess = None - CompilerOptions = compilerOptions - Verbosity = Verbosity.Normal } - - let mutable private state = State.Create(cliArgs, recompileAllFiles=true) - - let compile settings source = + type private CachedProject(name: string) = + let projDir = IO.Path.Join(__SOURCE_DIRECTORY__, "../" + name) |> Path.normalizeFullPath + let projFile = IO.Path.Join(projDir, name + ".fsproj") |> Path.normalizeFullPath + + let cliArgs = + let compilerOptions = CompilerOptionsHelper.Make() + { CliArgs.ProjectFile = projFile + FableLibraryPath = None + RootDir = projDir + Configuration = "Debug" + OutDir = None + IsWatch = false + Precompile = false + PrecompiledLib = None + PrintAst = false + SourceMaps = false + SourceMapsRoot = None + NoRestore = false + NoCache = false + NoGitignore = false + NoParallelTypeCheck = false + Exclude = ["Fable.Core"] + Replace = Map.empty + RunProcess = None + CompilerOptions = compilerOptions + Verbosity = Verbosity.Normal } + + let mutable state = State.Create(cliArgs, recompileAllFiles=true) + + member _.Compile(files: (string * string) list) = + for (fileName, content) in files do + IO.File.WriteAllText(IO.Path.Join(projDir, fileName), content) + + let result = + state + |> startCompilationAsync + |> Async.RunSynchronously + + match result with + | Error(_msg, logs) -> Array.toList logs + | Ok(newState, logs) -> + state <- newState + Array.toList logs + + let private project = CachedProject("TestProject") + let private projectWithSignature = CachedProject("TestProjectSignature") + + let private emptyLibrary = "module Library" + Environment.NewLine + + let private programFile settings source = let preamble = [ yield! settings.Opens |> List.map (sprintf "open %s") @@ -65,19 +85,22 @@ module Compiler = ] |> String.concat Environment.NewLine - let source = preamble + source - IO.File.WriteAllText(sourceFile, source) + preamble + source - let result = - state - |> startCompilationAsync - |> Async.RunSynchronously + let compileWithLibrary settings (library: string) source = + project.Compile [ + "Library.fs", library + "Program.fs", programFile settings source + ] - match result with - | Error(_msg, logs) -> Array.toList logs - | Ok(newState, logs) -> - state <- newState - Array.toList logs + let compile settings source = compileWithLibrary settings emptyLibrary source + + let compileWithSignedLibrary settings (signature: string) (library: string) source = + projectWithSignature.Compile [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] module Assert = open Util.Testing From d329a823298af9f934058f245d54155ccb555a69 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 1 Sep 2026 04:53:33 +0000 Subject: [PATCH 018/322] fix(rust): wait for the other files instead of sleeping before the last one. (#4927) --- src/Fable.Cli/Main.fs | 45 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 707771c415..03597f767a 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -502,6 +502,9 @@ type FableCompilerState = FilesCheckedButNotCompiled: Set FableFilesToCompileExpectedCount: int FableFilesCompiledCount: int + /// For Rust: the project's last file, held back until every other file + /// has compiled. See the note where it is set. + DelayedLastFile: string option FSharpLogs: LogEntry[] FableResults: FableCompileResult list HasFSharpCompilationFinished: bool @@ -521,6 +524,7 @@ type FableCompilerState = FilesCheckedButNotCompiled = Set.empty FableFilesToCompileExpectedCount = filesToCompile |> Array.filter isImplementationFile |> Array.length FableFilesCompiledCount = 0 + DelayedLastFile = None FSharpLogs = [||] FableResults = [] HasFSharpCompilationFinished = false @@ -623,14 +627,6 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl // It seems when there's a pair .fsi/.fs the F# compiler gives the .fsi extension to the implementation file let fileName = file.FileName |> Path.normalizePath |> Path.ensureFsExtension - // For Rust, delay last file's compilation so other files can finish compiling - if - projCracked.CliArgs.CompilerOptions.Language = Rust - && fileName = Array.last state.FilesToCompile - && state.FableFilesCompiledCount < state.FableFilesToCompileExpectedCount - 1 - then - do! Async.Sleep(1000) - Log.verbose ( lazy $"Type checked: {IO.Path.GetRelativePath(projCracked.CliArgs.RootDir, file.FileName)}" ) @@ -646,7 +642,28 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl state else let state = { state with FableProj = state.FableProj.Update([ file ]) } - fableCompile state fileName + + // For Rust the last file has to compile after every other + // one: it re-exports the namespaces the other files + // register into a shared registry while they compile, so + // compiling it early silently drops whatever had not + // registered yet. It is held back here and released by + // FableFileCompiled once the rest are done -- a wait on + // the actual condition, where the fixed sleep this + // replaces only shrank the race for small projects. + if + projCracked.CliArgs.CompilerOptions.Language = Rust + && fileName = Array.last state.FilesToCompile + && state.FableFilesCompiledCount < state.FableFilesToCompileExpectedCount - 1 + then + { state with + DelayedLastFile = Some fileName + // Marked pending so the finished check keeps + // waiting for it. + FilesCheckedButNotCompiled = Set.add fileName state.FilesCheckedButNotCompiled + } + else + fableCompile state fileName return! loop state @@ -691,6 +708,16 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl FilesCheckedButNotCompiled = Set.remove fileName state.FilesCheckedButNotCompiled } + // Everything else is compiled: release the held-back Rust + // last file (see FSharpFileTypeChecked). + let state = + match state.DelayedLastFile with + | Some delayed when + state.FableFilesCompiledCount = state.FableFilesToCompileExpectedCount - 1 + -> + fableCompile { state with DelayedLastFile = None } delayed + | _ -> state + if not state.IsSilent then let msg = let fileName = IO.Path.GetRelativePath(projCracked.CliArgs.RootDir, fileName) From fae5093f2801195e865adf1acfaec63f38bbf973 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Tue, 1 Sep 2026 13:21:29 +0000 Subject: [PATCH 019/322] feat(all): Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) --- src/Fable.Transforms/FableTransforms.fs | 82 +++++++++++-------- src/Fable.Transforms/QuotationEmitter.fs | 95 ++++++++++++---------- src/Fable.Transforms/Replacements.Util.fs | 46 +++++++++++ src/Fable.Transforms/Transforms.Util.fs | 10 ++- src/fable-library-beam/fable_quotation.erl | 39 +++++++++ src/fable-library-dart/quotation.dart | 67 +++++++++++++++ src/fable-library-rust/src/Quotation.fs | 46 +++++++++++ src/fable-library-ts/quotation.ts | 48 +++++++++++ tests/Beam/QuotationTests.fs | 31 +++++++ tests/Dart/src/QuotationTests.fs | 29 +++++++ tests/Js/Main/QuotationTests.fs | 49 +++++++++++ tests/Rust/tests/src/QuotationTests.fs | 57 +++++++++++++ 12 files changed, 524 insertions(+), 75 deletions(-) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index d638737185..f54ebf1564 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -255,40 +255,56 @@ let noSideEffectBeforeIdent identName expr = findIdentOrSideEffect expr && not sideEffect +/// A binding referenced from inside a quotation must not be inlined. `visit` +/// deliberately does not rewrite quoted expressions, so the substitution would +/// never reach that reference, and removing the binding would leave the captured +/// value dangling. +let isReferencedInsideQuote identName body = + body + |> deepExists ( + function + | Quote(quotedExpr, _, _) -> countReferencesUntil 1 identName quotedExpr >= 1 + | _ -> false + ) + let canInlineArg (com: Compiler) identName value body = - match value with - | Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true - | Value(StringConstant s, _) -> - match com.Options.Language with - | Python -> - // Only inline short strings if they're referenced at most once, - // to avoid duplicating the literal in generated code (which can cause - // issues like property access on string literals in Python) - s.Length < 100 && countReferencesUntil 2 identName body <= 1 - | _ -> s.Length < 100 - | _ -> - let refCount = countReferencesUntil 2 identName body - - // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) - // into closures: even though creation is side-effect-free, inlining into a closure - // called multiple times would create a new instance per call instead of sharing the - // single captured instance - let createsMutableState = - match value with - | Value(NewArray(_, _, kind), _) -> - match kind with - | MutableArray - | ResizeArray -> true - | ImmutableArray -> false - | _ -> false - - (refCount <= 1 - && not (canHaveSideEffects com value) - && not (createsMutableState && isIdentCaptured identName body)) - // If it can have side effects, make sure is at least referenced once so the expression is not erased - || (refCount = 1 - && noSideEffectBeforeIdent identName body - && not (isIdentCaptured identName body)) + if isReferencedInsideQuote identName body then + false + else + + match value with + | Value((Null _ | UnitConstant | TypeInfo _ | BoolConstant _ | NumberConstant _ | CharConstant _), _) -> true + | Value(StringConstant s, _) -> + match com.Options.Language with + | Python -> + // Only inline short strings if they're referenced at most once, + // to avoid duplicating the literal in generated code (which can cause + // issues like property access on string literals in Python) + s.Length < 100 && countReferencesUntil 2 identName body <= 1 + | _ -> s.Length < 100 + | _ -> + let refCount = countReferencesUntil 2 identName body + + // Don't inline values that create new mutable state (e.g. ResizeArray(), mutable arrays) + // into closures: even though creation is side-effect-free, inlining into a closure + // called multiple times would create a new instance per call instead of sharing the + // single captured instance + let createsMutableState = + match value with + | Value(NewArray(_, _, kind), _) -> + match kind with + | MutableArray + | ResizeArray -> true + | ImmutableArray -> false + | _ -> false + + (refCount <= 1 + && not (canHaveSideEffects com value) + && not (createsMutableState && isIdentCaptured identName body)) + // If it can have side effects, make sure is at least referenced once so the expression is not erased + || (refCount = 1 + && noSideEffectBeforeIdent identName body + && not (isIdentCaptured identName body)) /// Returns arity of lambda (or lambda option) types let (|Arity|) typ = diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 48ea46f98f..39dbb6add7 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -10,14 +10,20 @@ open Replacements.Util /// to construct a quotation AST. The input is the Fable.Expr captured /// inside a Quote node; the output is a Fable.Expr that calls the /// quotation runtime library to build the AST at runtime. -let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = +let rec private emitQuotedExprIn (com: Compiler) (bound: Set) (expr: Expr) : Expr = match expr with - | Value(kind, r) -> emitQuotedValue com kind r + | Value(kind, r) -> emitQuotedValue com bound kind r + + | IdentExpr ident when not (Set.contains ident.Name bound) -> + // Free in the quotation, so it is a local captured from the enclosing + // scope rather than a quotation variable. .NET splices the captured + // *value* in as a Value node; emitting a Var here instead left consumers + // with a name and no value to bind -- which is precisely what a query + // translator needs in order to turn a captured local into a parameter. + Helper.LibCall(com, "quotation", "mkValue", Any, [ IdentExpr ident; makeStrConst (typeToString ident.Type) ]) | IdentExpr ident -> - // Reference to a variable already introduced by a lambda/let in the quotation. - // Emit: quotation.mkVar(Var) - // We need a var reference. Create a var and then wrap it. + // Bound by a lambda or let inside the quotation: a genuine Var. let varExpr = Helper.LibCall( com, @@ -47,14 +53,14 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let bodyExpr = emitQuotedExpr com body + let bodyExpr = emitQuotedExprIn com (Set.add arg.Name bound) body Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; bodyExpr ]) | Delegate(args, body, _name, _tags) -> // Multi-arg delegate: nest as curried lambdas - let rec nestLambdas args body = + let rec nestLambdas bound args body = match args with - | [] -> emitQuotedExpr com body + | [] -> emitQuotedExprIn com bound body | (arg: Ident) :: rest -> let varExpr = Helper.LibCall( @@ -69,10 +75,10 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let innerBody = nestLambdas rest body + let innerBody = nestLambdas (Set.add arg.Name bound) rest body Helper.LibCall(com, "quotation", "mkLambda", Any, [ varExpr; innerBody ]) - nestLambdas args body + nestLambdas bound args body | Let(ident, value, body) -> let varExpr = @@ -88,25 +94,25 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = ] ) - let valueExpr = emitQuotedExpr com value - let bodyExpr = emitQuotedExpr com body + let valueExpr = emitQuotedExprIn com bound value + let bodyExpr = emitQuotedExprIn com (Set.add ident.Name bound) body Helper.LibCall(com, "quotation", "mkLet", Any, [ varExpr; valueExpr; bodyExpr ]) | IfThenElse(guardExpr, thenExpr, elseExpr, _r) -> - let guard = emitQuotedExpr com guardExpr - let thenE = emitQuotedExpr com thenExpr - let elseE = emitQuotedExpr com elseExpr + let guard = emitQuotedExprIn com bound guardExpr + let thenE = emitQuotedExprIn com bound thenExpr + let elseE = emitQuotedExprIn com bound elseExpr Helper.LibCall(com, "quotation", "mkIfThenElse", Any, [ guard; thenE; elseE ]) | CurriedApply(applied, args, _typ, _r) -> // Emit nested applications: Application(Application(f, a1), a2) - let appliedExpr = emitQuotedExpr com applied + let appliedExpr = emitQuotedExprIn com bound applied args |> List.fold (fun acc arg -> - let argExpr = emitQuotedExpr com arg + let argExpr = emitQuotedExprIn com bound arg Helper.LibCall(com, "quotation", "mkApplication", Any, [ acc; argExpr ]) ) appliedExpr @@ -114,7 +120,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | Call(callee, info, _typ, _r) -> let instanceExpr = match info.ThisArg with - | Some thisArg -> emitQuotedExpr com thisArg + | Some thisArg -> emitQuotedExprIn com bound thisArg // Static/operator call: no instance. | None -> mkNoInstanceExpr com @@ -135,16 +141,16 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = else let methodExpr = makeStrConst methodName let declTypeExpr = makeStrConst declaringType - let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExpr com)) + let argExprs = mkExprArray com (info.Args |> List.map (emitQuotedExprIn com bound)) Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; declTypeExpr ]) | Sequential exprs -> match exprs with - | [] -> emitQuotedExpr com (Value(UnitConstant, None)) - | [ single ] -> emitQuotedExpr com single + | [] -> emitQuotedExprIn com bound (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExprIn com bound single | first :: rest -> - let restExpr = emitQuotedExpr com (Sequential rest) - let firstExpr = emitQuotedExpr com first + let restExpr = emitQuotedExprIn com bound (Sequential rest) + let firstExpr = emitQuotedExprIn com bound first Helper.LibCall(com, "quotation", "mkSequential", Any, [ firstExpr; restExpr ]) | Operation(kind, _tags, _typ, _r) -> @@ -195,13 +201,13 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = let methodExpr = makeStrConst opName let instanceExpr = mkNoInstanceExpr com - let argExprs = mkExprArray com (args |> List.map (emitQuotedExpr com)) + let argExprs = mkExprArray com (args |> List.map (emitQuotedExprIn com bound)) // Operators have no declaring type; pass an empty string. Helper.LibCall(com, "quotation", "mkCall", Any, [ instanceExpr; methodExpr; argExprs; makeStrConst "" ]) | Get(expr, kind, _typ, _r) -> - let target = emitQuotedExpr com expr + let target = emitQuotedExprIn com bound expr match kind with | TupleIndex index -> Helper.LibCall(com, "quotation", "mkTupleGet", Any, [ target; makeIntConst index ]) @@ -227,8 +233,8 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Set(expr, kind, _typ, value, _r) -> - let target = emitQuotedExpr com expr - let valueExpr = emitQuotedExpr com value + let target = emitQuotedExprIn com bound expr + let valueExpr = emitQuotedExprIn com bound value match kind with | ValueSet -> @@ -242,7 +248,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | TypeCast(innerExpr, _typ) -> // Coerce/cast: just emit the inner expression for now - emitQuotedExpr com innerExpr + emitQuotedExprIn com bound innerExpr | DecisionTree(decisionExpr, targets) -> // Inline each DecisionTreeSuccess leaf into its target body via nested Lets, turning @@ -261,18 +267,18 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | e -> e ) - emitQuotedExpr com inlined + emitQuotedExprIn com bound inlined | DecisionTreeSuccess(idx, boundValues, _typ) -> match boundValues with - | [] -> emitQuotedExpr com (Value(UnitConstant, None)) - | [ single ] -> emitQuotedExpr com single + | [] -> emitQuotedExprIn com bound (Value(UnitConstant, None)) + | [ single ] -> emitQuotedExprIn com bound single | _ -> let msg = "Unsupported quotation node: DecisionTreeSuccess" Helper.LibCall(com, "quotation", "mkValue", Any, [ makeStrConst msg; makeStrConst "string" ]) | Test(testExpr, kind, _r) -> - let target = emitQuotedExpr com testExpr + let target = emitQuotedExprIn com bound testExpr // Note: real .NET quotations use dedicated UnionCaseTest/TypeTest node kinds here, not // Call — this emitter renders all of these as synthetic Calls instead (e.g. "get_IsCons", @@ -282,7 +288,7 @@ let rec emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = | UnionCaseTest tag -> // Represent as: (unionTag target) = tag let tagExpr = Helper.LibCall(com, "quotation", "mkUnionTag", Any, [ target ]) - let tagConst = emitQuotedExpr com (makeIntConst tag) + let tagConst = emitQuotedExprIn com bound (makeIntConst tag) Helper.LibCall( com, @@ -364,7 +370,7 @@ and private mkNoInstanceExpr (com: Compiler) : Expr = // so isCall/isFieldGet don't conflate the two. mkNullExpr com "novalue" -and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocation option) : Expr = +and private emitQuotedValue (com: Compiler) (bound: Set) (kind: ValueKind) (_r: SourceLocation option) : Expr = match kind with | BoolConstant b -> Helper.LibCall(com, "quotation", "mkValue", Any, [ makeBoolConst b; makeStrConst "bool" ]) @@ -388,7 +394,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio Helper.LibCall(com, "quotation", "mkValue", Any, [ Value(CharConstant c, None); makeStrConst "char" ]) | NewTuple(values, _isStruct) -> - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) Helper.LibCall(com, "quotation", "mkNewTuple", Any, [ emittedValues ]) @@ -398,7 +405,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | Some ent -> ent.FullName | None -> entRef.FullName - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) if com.Options.Language = Rust then // Rust needs the case name to build a real UnionCaseInfo; other targets keep (name, tag, fields). @@ -429,7 +437,8 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | Some ent -> ent.FSharpFields |> List.map (fun f -> makeStrConst f.Name) |> makeArray Any | None -> makeArray Any [] - let emittedValues = mkExprArray com (values |> List.map (emitQuotedExpr com)) + let emittedValues = + mkExprArray com (values |> List.map (emitQuotedExprIn com bound)) if com.Options.Language = Rust then // Rust needs the record type name; other targets keep (fieldNames, values). @@ -449,7 +458,7 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio match value with | Some v -> - let emitted = mkExprArray com [ emitQuotedExpr com v ] + let emitted = mkExprArray com [ emitQuotedExprIn com bound v ] Helper.LibCall( com, @@ -472,15 +481,15 @@ and private emitQuotedValue (com: Compiler) (kind: ValueKind) (_r: SourceLocatio | NewOption(value, _typ, _isStruct) -> match value with | Some v -> - let emitted = emitQuotedExpr com v + let emitted = emitQuotedExprIn com bound v Helper.LibCall(com, "quotation", "mkValue", Any, [ emitted; makeStrConst "option" ]) | None -> mkNullExpr com "option" | NewList(headAndTail, _typ) -> match headAndTail with | Some(head, tail) -> - let headExpr = emitQuotedExpr com head - let tailExpr = emitQuotedExpr com tail + let headExpr = emitQuotedExprIn com bound head + let tailExpr = emitQuotedExprIn com bound tail Helper.LibCall(com, "quotation", "mkNewList", Any, [ headExpr; tailExpr ]) | None -> mkNullExpr com "list" @@ -549,3 +558,7 @@ and private mkExprArray (com: Compiler) (elements: Expr list) : Expr = match elements with | [] when com.Options.Language = Rust -> Helper.LibCall(com, "quotation", "emptyExprArray", Any, []) | _ -> makeArray Any elements + +/// Entry point. Nothing is bound at the top of a quotation, so any identifier +/// still free by the time it is reached is a captured local. +let emitQuotedExpr (com: Compiler) (expr: Expr) : Expr = emitQuotedExprIn com Set.empty expr diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 04989077c5..17dbb09d2d 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1837,6 +1837,51 @@ module Quotations = Helper.LibCall(com, libModule, "isFieldGet", t, [ expr ], ?loc = r) |> Some | _ -> None + /// DerivedPatterns are the ones F# defines on top of Patterns. AndAlso and + /// OrElse matter because `&&` and `||` desugar to IfThenElse, so without them + /// a consumer has to re-derive that shape by hand; SpecificCall is how + /// operators are matched by identity rather than by compiled name. + let quotationDerivedPatterns + (libModule: string) + (com: ICompiler) + (ctx: Context) + r + (t: Type) + (i: CallInfo) + (_thisArg: Expr option) + (args: Expr list) + = + match i.CompiledName, args with + | ("AndAlsoPattern" | "|AndAlso|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isAndAlso", t, [ expr ], ?loc = r) |> Some + | ("OrElsePattern" | "|OrElse|_|"), [ expr ] -> + Helper.LibCall(com, libModule, "isOrElse", t, [ expr ], ?loc = r) |> Some + // SpecificCall is curried in FSharp.Core: applying it to the template + // returns the function that then tests the expression. Rather than have + // each runtime return a closure -- which the Rust backend uncurries back + // into a two-argument function -- the runtimes take both arguments and + // the partial application is built here. + | ("SpecificCallPattern" | "|SpecificCall|_|"), [ template ] -> + let exprType, resultType = + match t with + | LambdaType(argType, returnType) -> argType, returnType + | _ -> Any, Any + + let exprIdent = makeUniqueIdent com ctx exprType "expr" + + let body = + Helper.LibCall( + com, + libModule, + "isSpecificCall", + resultType, + [ template; IdentExpr exprIdent ], + ?loc = r + ) + + Lambda(exprIdent, body, None) |> Some + | _ -> None + let tryQuotationCall (libModule: string) (com: ICompiler) @@ -1853,6 +1898,7 @@ module Quotations = | Types.fsharpExprGeneric -> quotationExprs libModule com ctx r t info thisArg args | Types.fsharpVar -> quotationVars libModule com ctx r t info thisArg args | Types.patternsModule -> quotationPatterns libModule com ctx r t info thisArg args + | Types.derivedPatternsModule -> quotationDerivedPatterns libModule com ctx r t info thisArg args | "Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter" -> match info.CompiledName, args with | "EvaluateQuotation", [ expr ] -> Helper.LibCall(com, libModule, "evaluate", t, [ expr ], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 62009fc090..32eec7b374 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -492,6 +492,9 @@ module Types = [] let patternsModule = "Microsoft.FSharp.Quotations.PatternsModule" + [] + let derivedPatternsModule = "Microsoft.FSharp.Quotations.DerivedPatternsModule" + [] let printfModule = "Microsoft.FSharp.Core.PrintfModule" @@ -1801,7 +1804,12 @@ module AST = | None -> body :: (Option.toList finalizer) | DecisionTree(expr, targets) -> expr :: (List.map snd targets) | DecisionTreeSuccess(_, boundValues, _) -> boundValues - | Quote _ -> [] // Quoted expressions are opaque data — don't traverse + // Quoted expressions must not be *rewritten* (see `visit` above, which + // leaves them alone), but analysis has to see inside them: a local that + // is only referenced from within a quotation is still referenced, and + // returning [] here let reference counting treat it as dead and drop the + // binding out from under the captured value. + | Quote(quotedExpr, _, _) -> [ quotedExpr ] let deepExists (f: Expr -> bool) expr = let rec deepExistsInner (exprs: ResizeArray) = diff --git a/src/fable-library-beam/fable_quotation.erl b/src/fable-library-beam/fable_quotation.erl index 0abef8bb2a..4aa4d5c955 100644 --- a/src/fable-library-beam/fable_quotation.erl +++ b/src/fable-library-beam/fable_quotation.erl @@ -12,6 +12,7 @@ is_let/1, is_if_then_else/1, is_call/1, is_sequential/1, is_new_tuple/1, is_new_union_case/1, is_new_record/1, is_tuple_get/1, is_field_get/1, + is_and_also/1, is_or_else/1, is_specific_call/2, evaluate/1, expr_to_string/1, get_free_vars/1, substitute/2 ]). @@ -311,3 +312,41 @@ sub({expr, if_then_else, Guard, Then, Else}, Fn) -> sub({expr, sequential, First, Second}, Fn) -> {expr, sequential, sub(First, Fn), sub(Second, Fn)}; sub(E, _Fn) -> E. + +%% =================================================================== +%% DerivedPatterns +%% F# defines these on top of Patterns. AndAlso and OrElse recover the shape +%% `&&` and `||` desugar into; SpecificCall matches a call by the identity of a +%% template quotation rather than by compiled name. +%% =================================================================== + +%% a andalso b ==> if a then b else false +is_and_also({expr, if_then_else, G, T, {expr, value, false, _}}) -> {G, T}; +is_and_also(_) -> undefined. + +%% a orelse b ==> if a then true else b +is_or_else({expr, if_then_else, G, {expr, value, true, _}, E}) -> {G, E}; +is_or_else(_) -> undefined. + +%% A template such as <@ (=) @> quotes as nested lambdas around the call, so the +%% identifying Call node has to be dug out before comparing. +template_call({expr, lambda, _, Body}) -> template_call(Body); +template_call({expr, call, _, M, _, DT}) -> {M, DT}; +template_call(_) -> undefined. + +%% SpecificCall: returns {Instance, GenericArgs, Args}. The middle slot is F#'s +%% generic-argument list; this runtime models types as names rather than +%% System.Type, so it is always empty and callers match it with a wildcard. +%% Takes both arguments; the partial application FSharp.Core expresses by +%% currying is built by the replacement instead. +is_specific_call(Template, {expr, call, I, M, A, DT}) -> + case template_call(Template) of + {M, DT} -> + Instance = case I of + {expr, value, _, <<"novalue">>} -> undefined; + _ -> I + end, + {Instance, [], deref(A)}; + _ -> undefined + end; +is_specific_call(_, _) -> undefined. diff --git a/src/fable-library-dart/quotation.dart b/src/fable-library-dart/quotation.dart index 9ad67ebd39..a6c335c232 100644 --- a/src/fable-library-dart/quotation.dart +++ b/src/fable-library-dart/quotation.dart @@ -656,3 +656,70 @@ Expr substitute(Expr expr, dynamic fn) { return sub(expr); } + +// =================================================================== +// DerivedPatterns +// F# defines these on top of Patterns. AndAlso and OrElse recover the +// shape `&&` and `||` desugar into; SpecificCall matches a call by the +// identity of a template quotation rather than by compiled name. +// =================================================================== + +/// a && b ==> if a then b else false +types.Some>? isAndAlso(Expr expr) { + if (expr is ExprIfThenElse) { + final e = expr.elseExpr; + if (e is ExprValue && e.value == false) { + return types.Some>( + types.Tuple2(expr.guard, expr.thenExpr)); + } + } + return null; +} + +/// a || b ==> if a then true else b +types.Some>? isOrElse(Expr expr) { + if (expr is ExprIfThenElse) { + final t = expr.thenExpr; + if (t is ExprValue && t.value == true) { + return types.Some>( + types.Tuple2(expr.guard, expr.elseExpr)); + } + } + return null; +} + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +ExprCall? _templateCall(Expr expr) { + Expr current = expr; + while (current is ExprLambda) { + current = current.body; + } + return current is ExprCall ? current : null; +} + +// Takes both arguments; the partial application FSharp.Core expresses by +// currying is built by the replacement instead. +// Dart checks the full generic shape at runtime, so this spells out the type the +// F# signature declares: (Expr option * Type list * Expr list) option. +types.Some?, list.FSharpList, list.FSharpList>>? + isSpecificCall(Expr template, Expr expr) { + final wanted = _templateCall(template); + if (wanted == null || expr is! ExprCall) return null; + if (expr.method != wanted.method || + expr.declaringType != wanted.declaringType) { + return null; + } + final inst = expr.instance; + final normalized = + (inst is ExprValue && inst.type == 'novalue') ? null : inst; + // The middle slot is F#'s generic-argument list. This runtime models types as + // names rather than System.Type, so it is always empty; callers match it with + // a wildcard. + return types.Some< + types.Tuple3?, list.FSharpList, list.FSharpList>>( + types.Tuple3?, list.FSharpList, list.FSharpList>( + normalized == null ? null : types.Some(normalized), + list.ofArray(const []), + list.ofArray(expr.args))); +} diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs index 4cd3b281b7..feae382ce1 100644 --- a/src/fable-library-rust/src/Quotation.fs +++ b/src/fable-library-rust/src/Quotation.fs @@ -583,3 +583,49 @@ let evaluate (e: FSharpExpr) : obj = box 0 eval (ref Map.empty) e + +// --- DerivedPatterns ------------------------------------------------------ +// F# defines these on top of Patterns. AndAlso and OrElse recover the shape +// `&&` and `||` desugar into; SpecificCall matches a call by the identity of a +// template quotation rather than by compiled name. + +let private isBoolLit (want: bool) (e: FSharpExpr) = + match e with + | ExprValue(value, "bool") -> unbox value = want + | _ -> false + +/// a && b ==> if a then b else false +let isAndAlso (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) when isBoolLit false el -> Some(g, t) + | _ -> None + +/// a || b ==> if a then true else b +let isOrElse (e: FSharpExpr) = + match e with + | ExprIfThenElse(g, t, el) when isBoolLit true t -> Some(g, el) + | _ -> None + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +let rec private templateCall (e: FSharpExpr) = + match e with + | ExprLambda(_, body) -> templateCall body + | ExprCall(_, m, _, dt) -> Some(m, dt) + | _ -> None + +/// Takes both arguments; the partial application that FSharp.Core expresses by +/// currying is built by the replacement instead (see quotationDerivedPatterns). +let isSpecificCall (template: FSharpExpr) (e: FSharpExpr) = + match templateCall template, e with + | Some(wantedName, wantedType), ExprCall(instance, m, args, dt) when m = wantedName && dt = wantedType -> + let inst = + match instance with + | ExprValue(_, "novalue") -> None + | _ -> Some instance + + // The middle slot is F#'s generic-argument list (Type list), which erases + // to obj on targets with no System.Type runtime, so the element type has + // to be obj rather than anything narrower. + Some(inst, (List.empty), List.ofArray args) + | _ -> None diff --git a/src/fable-library-ts/quotation.ts b/src/fable-library-ts/quotation.ts index bf934c8859..34affe1a8d 100644 --- a/src/fable-library-ts/quotation.ts +++ b/src/fable-library-ts/quotation.ts @@ -664,3 +664,51 @@ export function exprFromJSON(json: any): Expr { default: return new ExprValue(json, "unknown"); } } + +// =================================================================== +// DerivedPatterns +// F# defines these on top of Patterns. AndAlso and OrElse recover the +// shape `&&` and `||` desugar into; SpecificCall matches a call by the +// identity of a template quotation rather than by compiled name. +// =================================================================== + +export function isAndAlso(expr: Expr): [Expr, Expr] | undefined { + // a && b ==> if a then b else false + if (expr instanceof ExprIfThenElse + && expr.elseExpr instanceof ExprValue + && expr.elseExpr.value === false) { + return [expr.guard, expr.thenExpr]; + } + return undefined; +} + +export function isOrElse(expr: Expr): [Expr, Expr] | undefined { + // a || b ==> if a then true else b + if (expr instanceof ExprIfThenElse + && expr.thenExpr instanceof ExprValue + && expr.thenExpr.value === true) { + return [expr.guard, expr.elseExpr]; + } + return undefined; +} + +/// A template such as <@ (=) @> quotes as nested lambdas around the call, so +/// the identifying Call node has to be dug out before comparing. +function templateCall(expr: Expr): ExprCall | undefined { + let current: Expr = expr; + while (current instanceof ExprLambda) { current = current.body; } + return current instanceof ExprCall ? current : undefined; +} + +// Takes both arguments; the partial application FSharp.Core expresses by +// currying is built by the replacement instead. +export function isSpecificCall(template: Expr, expr: Expr): [Expr | null, FSharpList, FSharpList] | undefined { + const wanted = templateCall(template); + if (wanted === undefined || !(expr instanceof ExprCall)) return undefined; + if (expr.method !== wanted.method || expr.declaringType !== wanted.declaringType) return undefined; + const instance = (expr.instance instanceof ExprValue && expr.instance.type === "novalue") ? null : expr.instance; + // The middle slot is F#'s generic-argument list. The runtime models types as + // names rather than System.Type, so it is always empty here; callers written + // against Fable match it with a wildcard. + return [instance, ofArray([]), ofArray(expr.args)]; +} diff --git a/tests/Beam/QuotationTests.fs b/tests/Beam/QuotationTests.fs index f0a5d41a22..6e29661cdd 100644 --- a/tests/Beam/QuotationTests.fs +++ b/tests/Beam/QuotationTests.fs @@ -4,6 +4,7 @@ open Fable.Tests.Util open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type QuotationTestUnion = @@ -283,3 +284,33 @@ let ``test Call on an instance whose value is null keeps a Some instance`` () = if not (hasSomeInstancePropertyGet q) then failwith "Expected a PropertyGet with a Some instance, even though its value is null" + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + +[] +let ``test DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" diff --git a/tests/Dart/src/QuotationTests.fs b/tests/Dart/src/QuotationTests.fs index 5553bd3bec..cec666b682 100644 --- a/tests/Dart/src/QuotationTests.fs +++ b/tests/Dart/src/QuotationTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.Dart.Quotation open Util open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type C = @@ -14,6 +15,27 @@ type Cfg = type MutableRec = { mutable Value: int } + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + let tests() = testCase "Simple integer value quotation" <| fun () -> let q = <@ 42 @> @@ -217,3 +239,10 @@ let tests() = <@ let r = { Value = 1 }: MutableRec in r.Value <- 5; r.Value @> equal 5 (result :?> int) + + testCase "DerivedPatterns AndAlso, OrElse and SpecificCall work" <| fun () -> + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" diff --git a/tests/Js/Main/QuotationTests.fs b/tests/Js/Main/QuotationTests.fs index 4149b9ba92..9e603a7b20 100644 --- a/tests/Js/Main/QuotationTests.fs +++ b/tests/Js/Main/QuotationTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Quotation open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Util.Testing #if FABLE_COMPILER @@ -19,6 +20,27 @@ type QuotationIndexerTest(values: int[]) = member _.Item with get (i: int) = values.[i] + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + let tests = testList "Quotations" [ #if FABLE_COMPILER @@ -272,4 +294,31 @@ let tests = // Thoth Auto format: ["Call", ...] parsed[0] |> unbox |> equal "Call" #endif + testCase "DerivedPatterns AndAlso, OrElse and SpecificCall work" <| fun () -> + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + + // A local captured by a quotation is spliced in as a Value holding its value, + // the way .NET does. It used to come through as a Var carrying only a name. + testCase "a captured local is a Value, not a Var" <| fun () -> + let captured = "SE" + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + // Checks that a bound variable is still a Var, not its name: JavaScript + // renames the lambda argument (x -> x_10), so the name is not portable. + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var _) -> "Var" + | _ -> "?") + |> equal "Var" ] diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs index 984a684dd7..6a3312a302 100644 --- a/tests/Rust/tests/src/QuotationTests.fs +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -3,6 +3,7 @@ module Fable.Tests.QuotationTests open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers // NOTE: quotations are deconstructed through an untyped `Expr` parameter (helpers below @@ -405,3 +406,59 @@ let ``Quotation Substitute replaces variables`` () = | Lambda(_, Call(_, _, [ Value(value, _); _ ])) -> unbox value |> equal 42 | _ -> failwith "Expected the lambda variable to be substituted" + +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + +[] +let ``DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + +// A local captured by a quotation is spliced in by .NET as a Value node holding +// its value. It used to come through as a Var carrying only a name, because the +// emitter treated every identifier as a quotation variable -- and the binding was +// then inlined away underneath it, since neither reference counting nor the +// inliner looked inside a Quote. +[] +let ``a captured local is a Value, not a Var`` () = + let captured = "SE" + + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + // a variable genuinely bound by the quotation stays a Var + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var v) -> v.Name + | _ -> "?") + |> equal "x" From f6d939a14c67010cb6cc02c7994d6f56c7156dab Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 1 Sep 2026 17:40:34 +0200 Subject: [PATCH 020/322] feat(python): support quotation derived patterns (#4928) --- .../fable_library/quotation.py | 45 ++++++++++++++++ tests/Python/TestQuotation.fs | 53 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/fable-library-py/fable_library/quotation.py b/src/fable-library-py/fable_library/quotation.py index 1063607ad5..8f2a821dd3 100644 --- a/src/fable-library-py/fable_library/quotation.py +++ b/src/fable-library-py/fable_library/quotation.py @@ -653,3 +653,48 @@ def sub(e: Expr) -> Expr: return e return sub(expr) + + +# =================================================================== +# DerivedPatterns +# F# defines these on top of Patterns. AndAlso and OrElse recover the +# shape `&&` and `||` desugar into; SpecificCall matches a call by the +# identity of a template quotation rather than by compiled name. +# =================================================================== + + +def is_and_also(expr: Expr) -> tuple[Expr, Expr] | None: + """Match `a && b`, represented as `if a then b else false`.""" + if isinstance(expr, ExprIfThenElse) and isinstance(expr.else_expr, ExprValue) and expr.else_expr.value is False: + return (expr.guard, expr.then_expr) + return None + + +def is_or_else(expr: Expr) -> tuple[Expr, Expr] | None: + """Match `a || b`, represented as `if a then true else b`.""" + if isinstance(expr, ExprIfThenElse) and isinstance(expr.then_expr, ExprValue) and expr.then_expr.value is True: + return (expr.guard, expr.else_expr) + return None + + +def _template_call(expr: Expr) -> ExprCall | None: + """Find the call identifying a template quotation under its lambdas.""" + current = expr + while isinstance(current, ExprLambda): + current = current.body + return current if isinstance(current, ExprCall) else None + + +def is_specific_call(template: Expr, expr: Expr) -> tuple[Expr | None, FSharpList[str], FSharpList[Expr]] | None: + """Match a call by the method identity represented by a template quotation.""" + wanted = _template_call(template) + if not isinstance(expr, ExprCall) or wanted is None: + return None + if expr.method != wanted.method or expr.declaring_type != wanted.declaring_type: + return None + + instance = None if isinstance(expr.instance, ExprValue) and expr.instance.type == "novalue" else expr.instance + # The middle slot is F#'s generic-argument list. This runtime models types as + # names rather than System.Type, so it is always empty; callers match it with + # a wildcard. + return (instance, of_array(Array([])), of_array(expr.args)) diff --git a/tests/Python/TestQuotation.fs b/tests/Python/TestQuotation.fs index f0a5d41a22..775cfff491 100644 --- a/tests/Python/TestQuotation.fs +++ b/tests/Python/TestQuotation.fs @@ -4,6 +4,7 @@ open Fable.Tests.Util open Util.Testing open Microsoft.FSharp.Quotations open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns open Microsoft.FSharp.Linq.RuntimeHelpers type QuotationTestUnion = @@ -12,6 +13,26 @@ type QuotationTestUnion = let inline quotTestDouble x = x * 2 +// DerivedPatterns: AndAlso and OrElse recover the shape `&&` and `||` desugar +// into, and SpecificCall matches a call by the identity of a template quotation +// rather than by its compiled name. All three previously reported +// "not supported by Fable". +type private DpRec = { Country: string; Balance: float; Id: int } + +let rec private dpToSql (e: Expr) : string = + match e with + | Lambda(_, body) -> dpToSql body + | AndAlso(l, r) -> "(" + dpToSql l + " AND " + dpToSql r + ")" + | OrElse(l, r) -> "(" + dpToSql l + " OR " + dpToSql r + ")" + | SpecificCall <@ (=) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " = " + dpToSql r + ")" + | SpecificCall <@ (>) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " > " + dpToSql r + ")" + | SpecificCall <@ (<) @> (_, _, [ l; r ]) -> "(" + dpToSql l + " < " + dpToSql r + ")" + // Bound as a wildcard: PropertyGet exposes a PropertyInfo on Rust but the + // bare name on the other targets, and this test is about DerivedPatterns. + | PropertyGet _ -> "col" + | Value _ -> "?" + | _ -> "" + [] let ``test Simple integer value quotation`` () = let q = <@ 42 @> @@ -283,3 +304,35 @@ let ``test Call on an instance whose value is null keeps a Some instance`` () = if not (hasSomeInstancePropertyGet q) then failwith "Expected a PropertyGet with a Some instance, even though its value is null" + +[] +let ``test DerivedPatterns AndAlso, OrElse and SpecificCall work`` () = + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" + + dpToSql <@ fun (c: DpRec) -> c.Country = "UK" && c.Balance > 100.0 @> + |> equal "((col = ?) AND (col > ?))" + + dpToSql <@ fun (c: DpRec) -> c.Id < 5 || c.Balance > 1.0 @> + |> equal "((col < ?) OR (col > ?))" + +[] +let ``test a captured local is a Value, not a Var`` () = + let captured = "SE" + + let describe (e: Expr) = + match e with + | Lambda(_, body) -> + match body with + | Value(v, _) -> "Value:" + unbox v + | Var v -> "Var:" + v.Name + | _ -> "other" + | _ -> "not a lambda" + + describe <@ fun (_: int) -> captured @> |> equal "Value:SE" + describe <@ fun (_: int) -> "SE" @> |> equal "Value:SE" + + // Checks that a bound variable remains a Var, rather than being spliced in. + (match <@ fun (x: string) -> x @> with + | Lambda(_, Var _) -> "Var" + | _ -> "?") + |> equal "Var" From 3e6bc9bd4d4337e901cc748f35df2a5933f0ba55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:01:16 -0700 Subject: [PATCH 021/322] fix(rust): Cleanup AsyncBuilder and TaskBuilder (#4936) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 8 +- src/Fable.Transforms/Rust/Replacements.fs | 117 ++-- src/fable-library-rust/src/Async.rs | 528 +++++++++++------- tests/Rust/Fable.Tests.Rust.fsproj | 2 +- tests/Rust/tests/src/AsyncTests.fs | 496 ++++++++-------- tests/Rust/tests/src/TaskTests.fs | 81 ++- tests/Rust/tests/src/UnionTests.fs | 18 + .../TypeScript/Fable.Tests.TypeScript.fsproj | 1 + 8 files changed, 759 insertions(+), 492 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 10134dafbf..4e3c20f7a5 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1111,7 +1111,8 @@ module TypeInfo = // // primitiveType name |> makeLrcPtrTy com ctx // else if isInferredGenericParam com ctx name isMeasure then - mkInferTy () // mkNeverTy () + // mkNeverTy () // unstable + mkInferTy () else primitiveType name @@ -1202,6 +1203,9 @@ module TypeInfo = | Replacements.Util.IsEntity ("Microsoft.FSharp.Control.FSharpEvent`2") (_, [ _delegateType; argsType ]) -> transformImportType com ctx [ argsType ] "Event" "FSharpEvent`2" + | Replacements.Util.IsEntity ("Microsoft.FSharp.Core.CompilerServices.ResumableStateMachine`1") (_, [ t ]) -> + mkUnitTy () // spurious ResumableStateMachine type is erased to unit type + // implemented regex types | Replacements.Util.IsEntity (Types.regexMatch) (_, []) -> transformImportType com ctx [] "RegExp" "Match" | Replacements.Util.IsEntity (Types.regexGroup) (_, []) -> transformImportType com ctx [] "RegExp" "Group" @@ -3198,7 +3202,7 @@ module Util = let nonTailStmts = revRest |> List.rev - |> List.map (fun e -> transformLeaveContext com ctx None e |> mkSemiStmt) + |> List.map (fun e -> com.TransformExpr(ctx, e) |> mkSemiStmt) nonTailStmts @ [ transformAsStmt com ctx last ] diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 21195cf623..347f42d82b 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -3243,17 +3243,28 @@ let mailbox (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "Reply" -> makeInstanceCall r t i callee "reply" args |> Some | _ -> None +let tryGetAsyncDelayBinder = + function + | MaybeCasted(Call(Import({ Selector = "AsyncBuilder_::delay" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ binder ] -> Some binder + | _ -> None + | _ -> None + let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | "Singleton", _, _ -> Value(UnitConstant, r) |> Some - //makeImportLib com t "singleton" "AsyncBuilder" |> Some - // For Using we need to cast the argument to IDisposable - | "Using", Some callee, [ arg; f ] -> makeInstanceCall r t i callee "Using" [ arg; f ] |> Some + | "Bind", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "Combine", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "combine", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | "Delay", _, _ -> Helper.LibCall(com, "AsyncBuilder", "delay", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "Bind", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) + | "For", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "for_loop", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "Return", _, _ -> Helper.LibCall(com, "AsyncBuilder", "r_return", t, args, i.SignatureArgTypes, ?loc = r) @@ -3261,6 +3272,24 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp | "ReturnFrom", _, _ -> Helper.LibCall(com, "AsyncBuilder", "return_from", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "TryFinally", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "try_finally", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "TryWith", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "try_with", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "Using", _, _ -> + Helper.LibCall(com, "AsyncBuilder", "using", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "While", _, [ guard; body ] -> + match tryGetAsyncDelayBinder body with + | Some body -> + Helper.LibCall(com, "AsyncBuilder", "while_loop", t, [ guard; body ], i.SignatureArgTypes, ?loc = r) + |> Some + | None -> + "Async builder While expects a delayed body." + |> addErrorAndReturnNull com ctx.InlinePath r + |> Some | "Zero", _, _ -> Helper.LibCall(com, "AsyncBuilder", "zero", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3288,49 +3317,58 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis Helper.LibCall(com, "Async", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some +let tryGetTaskDelayBinder = + function + | MaybeCasted(Call(Import({ Selector = "TaskBuilder_::delay" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ binder ] -> Some binder + | _ -> None + | _ -> None + let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // | "Run", _, _ -> Helper.LibCall(com, "Task", "run", t, args, ?loc=r) |> Some - // | "Singleton", _, _ -> makeImportLib com t "singleton" "TaskBuilder" |> Some | ".ctor", None, _ -> makeImportLib com t "new" "TaskBuilder" |> Some + | "task", _, _ -> Helper.LibCall(com, "TaskBuilder", "new", t, [], ?loc = r) |> Some | "Run", Some callee, _ -> makeInstanceCall r t i callee "run" args |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) + | ("Bind" | "TaskBuilderBase.Bind"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) |> Some - -let taskBuilderB (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - | "Bind", _, _ -> - Helper.LibCall(com, "Task", "bind", t, args, i.SignatureArgTypes, ?loc = r) + | ("Combine" | "TaskBuilderBase.Combine"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "combine", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "Return", _, _ -> - Helper.LibCall(com, "Task", "r_return", t, args, i.SignatureArgTypes, ?loc = r) + | ("Delay" | "TaskBuilderBase.Delay"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "delay", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "Delay", _, _ -> - Helper.LibCall(com, "Task", "delay", t, args, i.SignatureArgTypes, ?loc = r) + | ("For" | "TaskBuilderBase.For"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "for_loop", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "Zero", _, _ -> - Helper.LibCall(com, "Task", "zero", t, args, i.SignatureArgTypes, ?loc = r) + | ("Return" | "TaskBuilderBase.Return"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "r_return", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | _ -> None - -let taskBuilderHP (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - | "TaskBuilderBase.Bind", _, _ -> - Helper.LibCall(com, "Task", "bind", t, args, i.SignatureArgTypes, ?loc = r) + | ("ReturnFrom" | "TaskBuilderBase.ReturnFrom"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "return_from", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "TaskBuilderBase.Zero", _, _ -> - Helper.LibCall(com, "Task", "zero", t, args, i.SignatureArgTypes, ?loc = r) + | ("TryFinally" | "TaskBuilderBase.TryFinally"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "try_finally", t, args, i.SignatureArgTypes, ?loc = r) |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) + | ("TryWith" | "TaskBuilderBase.TryWith"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "try_with", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | ("Using" | "TaskBuilderBase.Using"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "using", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | ("While" | "TaskBuilderBase.While"), _, [ guard; body ] -> + match tryGetTaskDelayBinder body with + | Some body -> + Helper.LibCall(com, "TaskBuilder", "while_loop", t, [ guard; body ], i.SignatureArgTypes, ?loc = r) + |> Some + | None -> + "Task builder While expects a delayed body." + |> addErrorAndReturnNull com ctx.InlinePath r + |> Some + | ("Zero" | "TaskBuilderBase.Zero"), _, _ -> + Helper.LibCall(com, "TaskBuilder", "zero", t, args, i.SignatureArgTypes, ?loc = r) |> Some - -let taskBuilderM (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName, thisArg, args with - | "task", _, _ -> Helper.LibCall(com, "TaskBuilder", "new", t, [], ?loc = r) |> Some | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some | meth, None, _ -> Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) @@ -3788,10 +3826,11 @@ let private replacedModules = "Microsoft.FSharp.Control.AsyncActivation`1", asyncBuilder "Microsoft.FSharp.Control.FSharpAsync", asyncs "Microsoft.FSharp.Control.AsyncPrimitives", asyncs - "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilderM + "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilder "Microsoft.FSharp.Control.TaskBuilder", taskBuilder - "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilderB - "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilderHP + "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.LowPriority", taskBuilder Types.guid, guids "System.Uri", uris "System.Lazy`1", laziness diff --git a/src/fable-library-rust/src/Async.rs b/src/fable-library-rust/src/Async.rs index 1f104d3d7f..7cce6256ab 100644 --- a/src/fable-library-rust/src/Async.rs +++ b/src/fable-library-rust/src/Async.rs @@ -12,23 +12,12 @@ pub mod Async_ { use futures_timer::Delay; use super::Task_::Task; - use crate::NativeArray_::{array_from, Array}; use crate::Native_::{Func0, Seq}; + use crate::NativeArray_::{Array, array_from}; use crate::System::Threading::CancellationToken; pub struct Async { pub future: Arc + Send + Sync>>>>, - - /// How to build this computation again, where there is such a thing. - /// - /// An F# `Async` is a description that can be run any number of - /// times; a Rust `Future` can be polled to completion exactly once. - /// `while` and `for` inside an async block have to run their body once - /// per iteration, so `AsyncBuilder_::delay` -- the one place the F# - /// compiler hands over a re-runnable description, and which wraps every - /// loop body -- keeps its builder here. Everything else leaves it None - /// and stays single-shot exactly as before. - pub restart: Option>>>, } impl Async { @@ -38,7 +27,6 @@ pub mod Async_ { Arc::from(Async { future: Arc::from(Mutex::from(b)), - restart: None, }) } } @@ -91,7 +79,20 @@ pub mod Async_ { let res = res.as_mut().await; res }; - executor::block_on(unitFut) + if executor::enter().is_err() { + thread::spawn(|| executor::block_on(unitFut)) + .join() + .expect("nested async computation panicked") + } else { + executor::block_on(unitFut) + } + } + + pub fn startImmediate( + a: Arc>, + cancellationToken: Option, + ) { + let _ = runSynchronously(a, None, cancellationToken); } /// Async.Parallel. Runs every computation on one executor and collects the @@ -101,7 +102,8 @@ pub mod Async_ { computations: Seq>>, ) -> Arc>> { // F# hands this a seq, so materialise it before anything is awaited. - let items: Vec>> = crate::Seq_::toArray(computations).iter().cloned().collect(); + let items: Vec>> = + crate::Seq_::toArray(computations).iter().cloned().collect(); let fut = async move { let futures = items.into_iter().map(|a| async move { @@ -116,6 +118,28 @@ pub mod Async_ { Async::from_future(fut) } + /// Async.Sequential. Runs each computation to completion before starting + /// the next one and collects the results in order. + pub fn sequential( + computations: Seq>>, + ) -> Arc>> { + let items: Vec>> = + crate::Seq_::toArray(computations).iter().cloned().collect(); + + let fut = async move { + let mut results = Vec::with_capacity(items.len()); + + for a in items { + let mut guard = a.future.lock().await; + results.push(guard.as_mut().await); + } + + array_from(results) + }; + + Async::from_future(fut) + } + /// Async.StartImmediateAsTask. Runs on the calling thread rather than /// queueing onto the thread pool, so it does not pay for a hand-off. The /// returned task is therefore already complete: there is no scheduler here @@ -146,8 +170,8 @@ pub mod AsyncBuilder_ { use super::Async_::Async; use crate::Exception_::get_exception; - use crate::Native_::{eraseUnitArg, EraseUnitArg, Func0, Func1, LrcPtr, Seq}; - use crate::System::Exception; + use crate::Native_::{EraseUnitArg, Func0, Func1, LrcPtr, Seq, eraseUnitArg}; + use crate::System::{Exception, IDisposable}; /// Awaits one computation to completion. /// @@ -158,148 +182,107 @@ pub mod AsyncBuilder_ { fut.as_mut().await } - /// The `async` builder value. - /// - /// Fable resolves `DefaultAsyncBuilder` to this and emits every builder - /// method it does not map to a module function -- Combine, While, For, - /// TryWith, TryFinally, Using -- as an instance call on it. Without the - /// value and the methods, any async block containing a computation in - /// statement position failed to compile. + /// The `async` builder value used to resolve `DefaultAsyncBuilder`. pub struct AsyncBuilder {} pub static singleton: AsyncBuilder = AsyncBuilder {}; - impl AsyncBuilder { - /// Two computations in sequence, discarding the first result. This is - /// what `if cond then do! x` followed by anything else compiles to. - pub fn Combine( - &self, - computation1: Arc>, - computation2: Arc>, - ) -> Arc> { - Async::from_future(async move { - run(computation1).await; - run(computation2).await - }) - } - - /// `while` inside an async block. - /// - /// The body has to run once per iteration, and a future can only be - /// polled to completion once, so each lap asks `restart` for a fresh - /// one. The F# compiler always wraps a loop body in `Delay`, which is - /// what sets it. - pub fn While(&self, guard: Func0, computation: Arc>) -> Arc> { - Async::from_future(async move { - let mut lap = 0usize; - - while guard() { - let body = match &computation.restart { - Some(build) => build(), - // Nothing to rebuild from. One lap can still be served - // by the computation itself; a second would poll a - // future that has already completed, so say so rather - // than panicking from inside the future. - None if lap == 0 => computation.clone(), - None => panic!( - "async while: the loop body cannot be run more than once" - ), - }; + /// Two computations in sequence, discarding the first result. This is + /// what `if cond then do! x` followed by anything else compiles to. + pub fn combine( + computation1: Arc>, + computation2: Arc>, + ) -> Arc> { + Async::from_future(async move { + run(computation1).await; + run(computation2).await + }) + } - run(body).await; - lap += 1; - } - }) - } + /// `for` inside an async block. + /// + /// The sequence is materialised before anything is awaited, the same + /// way `Async.Parallel` does it: a Fable `Seq`'s iterator is not `Send` + /// and so cannot be held across an await. A lazy, side-effecting + /// sequence therefore runs to completion up front instead of + /// interleaving with the body. + pub fn for_loop( + sequence: Seq, + body: Func1>>, + ) -> Arc> { + let items: Vec = crate::Seq_::toArray(sequence).iter().cloned().collect(); + + Async::from_future(async move { + for item in items { + run(body(item)).await; + } + }) + } - /// `for` inside an async block. - /// - /// The sequence is materialised before anything is awaited, the same - /// way `Async.Parallel` does it: a Fable `Seq`'s iterator is not `Send` - /// and so cannot be held across an await. A lazy, side-effecting - /// sequence therefore runs to completion up front instead of - /// interleaving with the body. - pub fn For( - &self, - sequence: Seq, - body: Func1>>, - ) -> Arc> { - let items: Vec = crate::Seq_::toArray(sequence).iter().cloned().collect(); - - Async::from_future(async move { - for item in items { - run(body(item)).await; - } - }) - } + /// `try/with` inside an async block. + /// + /// Exceptions are panics on this target, and the body spans await + /// points, so the catch has to wrap the whole computation rather than a + /// synchronous call the way `Exception_::try_catch` does. That also + /// means the panic hook is left alone: it is process-global, and + /// swapping it for the duration of an await would silence unrelated + /// panics on other threads. A caught exception therefore still prints + /// the default panic message. + pub fn try_with( + computation: Arc>, + catchHandler: Func1, Arc>>, + ) -> Arc> { + Async::from_future(async move { + // The panic payload is only Send, never Sync, so it must not + // still be alive at the await below -- that would make this + // future non-Sync and `Async` requires Sync. Turning it into + // the handler's computation here ends its scope first. + let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { + Ok(value) => Ok(value), + Err(payload) => Err(catchHandler(get_exception(&*payload))), + }; + + match outcome { + Ok(value) => value, + Err(handled) => run(handled).await, + } + }) + } - /// `try/with` inside an async block. - /// - /// Exceptions are panics on this target, and the body spans await - /// points, so the catch has to wrap the whole computation rather than a - /// synchronous call the way `Exception_::try_catch` does. That also - /// means the panic hook is left alone: it is process-global, and - /// swapping it for the duration of an await would silence unrelated - /// panics on other threads. A caught exception therefore still prints - /// the default panic message. - pub fn TryWith( - &self, - computation: Arc>, - catchHandler: Func1, Arc>>, - ) -> Arc> { - Async::from_future(async move { - // The panic payload is only Send, never Sync, so it must not - // still be alive at the await below -- that would make this - // future non-Sync and `Async` requires Sync. Turning it into - // the handler's computation here ends its scope first. - let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { - Ok(value) => Ok(value), - Err(payload) => Err(catchHandler(get_exception(&*payload))), - }; - - match outcome { - Ok(value) => value, - Err(handled) => run(handled).await, - } - }) - } + /// `try/finally` inside an async block. The compensation runs on both + /// paths, and a failure keeps unwinding with its original payload. + pub fn try_finally( + computation: Arc>, + compensation: Func0<()>, + ) -> Arc> { + Async::from_future(async move { + let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; + compensation(); - /// `try/finally` inside an async block. The compensation runs on both - /// paths, and a failure keeps unwinding with its original payload. - pub fn TryFinally( - &self, - computation: Arc>, - compensation: Func0<()>, - ) -> Arc> { - Async::from_future(async move { - let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; - compensation(); - - match outcome { - Ok(value) => value, - Err(payload) => std::panic::resume_unwind(payload), - } - }) - } + match outcome { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } + }) + } - /// `use` inside an async block. - /// - /// Deliberately does not call Dispose. A plain `use` does not call it - /// on this target either -- resources go out with Rust's own Drop, see - /// the disabled cases in `tests/Rust/tests/src/MiscTests.fs` -- and - /// disposing here alone would make the async and non-async forms - /// disagree. This exists so `use` inside `async` compiles and binds, - /// which it previously did not. - pub fn Using< - D: Clone + Send + Sync + 'static, - U: Clone + Send + Sync + 'static, - >( - &self, - resource: D, - binder: Func1>>, - ) -> Arc> { - binder(resource) - } + /// `use` inside an async block. Dispose the resource on both success and + /// failure, preserving the original panic after cleanup. + pub fn using( + resource: D, + binder: Func1>>, + ) -> Arc> { + Async::from_future(async move { + let outcome = AssertUnwindSafe(async { run(binder(resource.clone())).await }) + .catch_unwind() + .await; + resource.Dispose(); + + match outcome { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + }) } /// `Delay` is what makes an async block a description rather than a running @@ -319,20 +302,26 @@ pub mod AsyncBuilder_ { /// takes its argument by value -- captured state from before the block ran. /// Nothing noticed while every async block was a straight line, because the /// value was read at construction and the block did nothing in between. - /// `Combine` makes it visible: in `while .. done; return i` the `return i` + /// `combine` makes it visible: in `while .. done; return i` the `return i` /// half was read before the loop had touched `i`. pub fn delay>>>( binder: F, ) -> Arc> { - let binder = eraseUnitArg(binder); - let build = binder.clone(); + let build = eraseUnitArg(binder); Arc::from(Async { future: Arc::from(Mutex::from(Box::pin(async move { run(build()).await }) as Pin + Send + Sync + 'static>>)), - // The only place F# gives us a way to build the computation again. - // While and For need one; nothing else looks at it. - restart: Some(binder), + }) + } + + /// `while` inside an async block. The body is supplied as a factory so a + /// fresh one-shot future is built for every iteration. + pub fn while_loop(guard: Func0, body: Func0>>) -> Arc> { + Async::from_future(async move { + while guard() { + run(body()).await; + } }) } @@ -427,21 +416,24 @@ pub mod Monitor_ { #[cfg(feature = "threaded")] pub mod Task_ { + use std::panic::AssertUnwindSafe; use std::pin::Pin; use std::sync::{Arc, Mutex, RwLock}; use std::task::{Poll, Waker}; use std::thread::{self, JoinHandle}; - use std::time::Duration; use futures::{Future, FutureExt}; use super::ThreadPool::try_init_and_get_pool; - use crate::Native_::{Func0, Func1}; + use crate::Exception_::get_exception; + use crate::Native_::LrcPtr; + use crate::System::Exception; pub enum TaskState { New(Pin + Send + Sync>>), Running, Complete(T), + Faulted(LrcPtr), } impl TaskState { @@ -494,8 +486,10 @@ pub mod Task_ { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll { - if let TaskState::Complete(res) = &*self.result.read().unwrap() { - return Poll::Ready(res.clone()); + match &*self.result.read().unwrap() { + TaskState::Complete(res) => return Poll::Ready(res.clone()), + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), + _ => {} } // Register before re-reading the state: set_result may complete the @@ -513,10 +507,16 @@ pub mod Task_ { } // Running: set_result wakes us. TaskState::Running => Poll::Pending, + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), } } } + pub fn from_result(value: T) -> Arc> { + let task = Task::from_result(value); + Arc::from(task) + } + impl Task { pub fn new(fut: impl Future + Send + Sync + 'static) -> Task { Task { @@ -538,6 +538,19 @@ pub mod Task_ { (*m) = TaskState::Complete(value); } + self.wake_waiters(); + } + + pub fn set_fault(&self, error: LrcPtr) { + { + let mut m = self.result.write().unwrap(); + (*m) = TaskState::Faulted(error); + } + + self.wake_waiters(); + } + + fn wake_waiters(&self) { // Drain first, then wake outside the lock: a waker may poll straight // back into this task. let wakers: Vec = self.wakers.lock().unwrap().drain(..).collect(); @@ -556,8 +569,10 @@ pub mod Task_ { } pub fn get_result(&self) -> T { - if let TaskState::Complete(res) = &*self.result.read().unwrap() { - return res.clone(); + match &*self.result.read().unwrap() { + TaskState::Complete(res) => return res.clone(), + TaskState::Faulted(error) => std::panic::panic_any(error.clone()), + _ => {} } // Parks on the waker rather than waking every 10ms to look again. @@ -572,8 +587,10 @@ pub mod Task_ { match ts { TaskState::New(mut fut) => { let f2 = async move { - let res = fut.as_mut().await; - t.set_result(res); + match AssertUnwindSafe(fut.as_mut()).catch_unwind().await { + Ok(res) => t.set_result(res), + Err(payload) => t.set_fault(get_exception(&*payload)), + } }; let pool = super::ThreadPool::try_init_and_get_pool(); //eprintln!("{:?} new task added to queue", thread::current().id()); @@ -582,6 +599,56 @@ pub mod Task_ { _ => {} } } + + pub async fn run(t: Arc>) -> T { + let state = t.result.write().unwrap().replace(TaskState::Running); + match state { + TaskState::New(mut fut) => { + match AssertUnwindSafe(fut.as_mut()).catch_unwind().await { + Ok(result) => { + t.set_result(result.clone()); + result + } + Err(payload) => { + t.set_fault(get_exception(&*payload)); + std::panic::resume_unwind(payload) + } + } + } + TaskState::Running => (&*t).await, + TaskState::Complete(value) => { + t.set_result(value.clone()); + value + } + TaskState::Faulted(error) => std::panic::panic_any(error), + } + } + } +} + +#[cfg(feature = "threaded")] +pub mod TaskBuilder_ { + use std::panic::AssertUnwindSafe; + use super::super::Native_::LrcPtr; + use super::Task_::Task; + use std::sync::Arc; + use crate::Native_::{Func0, Func1, Seq}; + use crate::Exception_::get_exception; + use crate::System::{Exception, IDisposable}; + use futures::FutureExt; + + pub struct TaskBuilder {} + + impl TaskBuilder { + pub fn run(&self, task: Arc>) -> Arc> { + Task::start(task.clone()); + task + } + + } + + pub fn new() -> LrcPtr { + LrcPtr::new(TaskBuilder {}) } pub fn bind( @@ -590,13 +657,10 @@ pub mod Task_ { ) -> Arc> { let next = async move { //eprintln!("{:?} begin await source fut", thread::current().id()); - let m = opt.as_ref().await; + let m = run(opt).await; //eprintln!("{:?} awaiting source future success", thread::current().id()); let nextAsync = binder(m); - if nextAsync.is_new() { - Task::start(nextAsync.clone()); - } - let next = nextAsync.as_ref().await; + let next = run(nextAsync).await; //eprintln!("{:?} setting result", thread::current().id()); next }; @@ -605,9 +669,84 @@ pub mod Task_ { Arc::from(task) } + async fn run(task: Arc>) -> T { + if task.is_new() { + Task::run(task).await + } else { + (&*task).await + } + } + + pub fn combine( + computation1: Arc>, + computation2: Arc>, + ) -> Arc> { + let task = Task::new(async move { + run(computation1).await; + run(computation2).await + }); + Arc::from(task) + } + + pub fn return_from(computation: Arc>) -> Arc> { + computation + } + + pub fn try_with( + computation: Arc>, + catchHandler: Func1, Arc>>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = match AssertUnwindSafe(run(computation)).catch_unwind().await { + Ok(value) => Ok(value), + Err(payload) => Err(get_exception(&*payload)), + }; + + match outcome { + Ok(value) => value, + Err(exception) => run(catchHandler(exception)).await, + } + }); + Arc::from(task) + } + + pub fn try_finally( + computation: Arc>, + compensation: Func0<()>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = AssertUnwindSafe(run(computation)).catch_unwind().await; + compensation(); + + match outcome { + Ok(value) => value, + Err(payload) => std::panic::resume_unwind(payload), + } + }); + Arc::from(task) + } + + pub fn using( + resource: D, + binder: Func1>>, + ) -> Arc> { + let task = Task::new(async move { + let outcome = AssertUnwindSafe(async { run(binder(resource.clone())).await }) + .catch_unwind() + .await; + resource.Dispose(); + + match outcome { + Ok(result) => result, + Err(payload) => std::panic::resume_unwind(payload), + } + }); + Arc::from(task) + } + pub fn delay(binder: Func0>>) -> Arc> { - let pr = binder(); - pr + let task = Task::new(async move { run(binder()).await }); + Arc::from(task) } pub fn r_return(item: T) -> Arc> { @@ -615,36 +754,35 @@ pub mod Task_ { Arc::from(t) } - pub fn zero() -> Arc> { + pub fn zero() -> Arc> { r_return(()) } - pub fn from_result(t: T) -> Arc> { - let t = Task { - result: Arc::from(RwLock::from(TaskState::Complete(t))), - wakers: Arc::from(Mutex::from(Vec::new())), - }; - Arc::from(t) - } -} - -#[cfg(feature = "threaded")] -pub mod TaskBuilder_ { - use super::super::Native_::LrcPtr; - use super::Task_::Task; - use std::sync::Arc; - - pub struct TaskBuilder {} + pub fn for_loop( + sequence: Seq, + body: Func1>>, + ) -> Arc> { + let arr = crate::Seq_::toArray(sequence); + let task = Task::new(async move { + let items = arr.iter().cloned(); + for item in items { + let next = body(item); + run(next).await; + } + }); - impl TaskBuilder { - pub fn run(&self, task: Arc>) -> Arc> { - Task::start(task.clone()); - task - } + Arc::from(task) } - pub fn new() -> LrcPtr { - LrcPtr::new(TaskBuilder {}) + pub fn while_loop(guard: Func0, body: Func0>>) -> Arc> { + let task = Task::new(async move { + while guard() { + let next = body(); + run(next).await; + } + }); + + Arc::from(task) } } diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 9c4f9900ea..471dd3770b 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -76,7 +76,7 @@ - + diff --git a/tests/Rust/tests/src/AsyncTests.fs b/tests/Rust/tests/src/AsyncTests.fs index b617f49823..2d12073243 100644 --- a/tests/Rust/tests/src/AsyncTests.fs +++ b/tests/Rust/tests/src/AsyncTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.AsyncTests open Util.Testing +open System open System.Threading open System.Threading.Tasks @@ -110,16 +111,16 @@ let shouldExecStartImmediateAsTask () = |> Async.RunSynchronously |> equal 7 -// [] -// let shouldMarshalMutOverAsyncClosureCorrectly () = -// let mutable x = 1 -// let comp = async { -// let! z = async { return 3 } -// x <- x + 1 -// return x + z -// } -// let t = Async.StartAsTask comp -// t.Result |> equal 5 +[] +let shouldMarshalMutOverAsyncClosureCorrectly () = + let mutable x = 1 + let comp = async { + let! z = async { return 3 } + x <- x + 1 + return x + z + } + let t = Async.StartAsTask comp + t.Result |> equal 5 [] type ArcRecord = { @@ -151,17 +152,16 @@ let shouldExecuteTask () = } comp.Result |> equal 3 -// TODO: Fix ResumableStateMachine issue on .NET 8.0 -// [] -// let shouldExecuteMutationOnTask () = -// let a = Task.FromResult 0 -// let mutable x = 0 -// let comp = task { -// let! _ = a -// x <- x + 1 -// } -// do comp.Result -// x |> equal 1 +[] +let shouldExecuteMutationOnTask () = + let a = Task.FromResult 0 + let mutable x = 0 + let comp = task { + let! _ = a + x <- x + 1 + } + do comp.Result + x |> equal 1 // [] // let ``should execute mutation on thread unsafe`` () = @@ -230,18 +230,18 @@ let ``Lock should return result`` () = let res = lock o (fun () -> { x = "42"}) res.x |> equal "42" -//[] -// let testShouldMutateAndLock () = -// let o = new System.Object() -// let mutable x = 1 -// let lazyGet = async { return 1 } -// let comp = async { -// let! _ = lazyGet -// lock o (fun () -> x <- x + 1) -// } -// let t = Async.StartAsTask comp -// do t.Result -// x |> equal 2 +[] +let testShouldMutateAndLock () = + let o = { x = "test" } + let mutable x = 1 + let lazyGet = async { return 1 } + let comp = async { + let! _ = lazyGet + lock o (fun () -> x <- x + 1) + } + let t = Async.StartAsTask comp + do t.Result + x |> equal 2 [] let ``Async.Sleep works`` () = @@ -287,35 +287,35 @@ let ``Async.Sleep works`` () = // res := true // }, token) -// let asyncMap f a = async { -// let! a = a -// return f a -// } +let asyncMap (f: int -> unit) (a: Async) : Async = async { + let! a = a + return f a +} -// [] -// let ``Simple async translates without exception`` () = -// async { return () } -// |> Async.StartImmediate +[] +let ``Simple async translates without exception`` () = + async { return () } + |> Async.StartImmediate -// [] -// let ``Async while binding works correctly`` () = -// let mutable result = 0 -// async { -// while result < 10 do -// result <- result + 1 -// } |> Async.StartImmediate -// equal result 10 +[] +let ``Async while binding works correctly`` () = + let mutable result = 0 + async { + while result < 10 do + result <- result + 1 + } |> Async.StartImmediate + equal result 10 -// [] -// let ``Async for binding works correctly`` () = -// let inputs = [|1; 2; 3|] -// let result = ref 0 -// async { -// for inp in inputs do -// result := !result + inp -// } |> Async.StartImmediate -// equal !result 6 +[] +let ``Async for binding works correctly`` () = + let inputs = [|1; 2; 3|] + let result = ref 0 + async { + for inp in inputs do + result := !result + inp + } |> Async.StartImmediate + equal !result 6 // [] // let ``Async exceptions are handled correctly`` () = @@ -330,18 +330,18 @@ let ``Async.Sleep works`` () = // !result // f true + f false |> equal 22 -// [] -// let ``Simple async is executed correctly`` () = -// let result = ref false -// let x = async { return 99 } -// async { -// let! x = x -// let y = 99 -// result := x = y -// } -// //TODO: RunSynchronously would make more sense here but in JS I think this will be ok. -// |> Async.StartImmediate -// equal !result true +[] +let ``Simple async is executed correctly`` () = + let result = ref false + let x = async { return 99 } + async { + let! x = x + let y = 99 + result := x = y + } + //TODO: RunSynchronously would make more sense here but in JS I think this will be ok. + |> Async.StartImmediate + equal !result true // [] // let ``async use statements should dispose of resources when they go out of scope`` () = @@ -360,29 +360,29 @@ let ``Async.Sleep works`` () = // step2ok := !isDisposed // (!step1ok && !step2ok) |> equal true -// [] -// let ``Try _ with _ expressions inside async expressions work the same`` () = -// let result = ref "" -// let throw() : unit = -// raise(exn "Boo!") -// let append(x) = -// result := !result + x -// let innerAsync() = -// async { -// append "b" -// try append "c" -// throw() -// append "1" -// with _ -> append "d" -// append "e" -// } -// async { -// append "a" -// try do! innerAsync() -// with _ -> append "2" -// append "f" -// } |> Async.StartImmediate -// equal !result "abcdef" +[] +let ``Try _ with _ expressions inside async expressions work the same`` () = + let result = ref "" + let throw() : unit = + raise(exn "Boo!") + let append(x) = + result := !result + x + let innerAsync() = + async { + append "b" + try append "c" + throw() + append "1" + with _ -> append "d" + append "e" + } + async { + append "a" + try do! innerAsync() + with _ -> append "2" + append "f" + } |> Async.StartImmediate + equal !result "abcdef" // // Disable this test for dotnet as it's failing too many times in Appveyor // #if FABLE_COMPILER @@ -471,89 +471,89 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Async.Parallel works`` () = -// async { -// let makeWork i = -// async { -// do! Async.Sleep 200 -// return i -// } -// let res: int[] ref = ref [||] -// let works = [makeWork 1; makeWork 2; makeWork 3] -// async { -// let! x = Async.Parallel works -// res := x -// } |> Async.StartImmediate -// do! Async.Sleep 500 -// !res |> Array.sum |> equal 6 -// } -// |> Async.StartImmediate - -// [] -// let ``Async.Parallel is lazy`` () = -// async { -// let mutable x = 0 -// let add i = -// #if FABLE_COMPILER -// x <- x + i -// #else -// Interlocked.Add(&x, i) |> ignore -// #endif -// let a = Async.Parallel [ -// async { add 1 } -// async { add 2 } -// ] -// do! Async.Sleep 100 -// equal 0 x -// let! _ = a -// equal 3 x -// } -// |> Async.StartImmediate +[] +let ``Async.Parallel works`` () = + async { + let makeWork (i: int): Async = + async { + do! Async.Sleep 200 + return i + } + let res: int[] ref = ref [||] + let works = [makeWork 1; makeWork 2; makeWork 3] + async { + let! x = Async.Parallel works + res := x + } |> Async.StartImmediate + do! Async.Sleep 500 + !res |> Array.sum |> equal 6 + } + |> Async.StartImmediate -// [] -// let ``Async.Sequential works`` () = -// async { -// let mutable _aggregate = 0 +[] +let ``Async.Parallel is lazy`` () = + async { + let mutable x = 0 + let add i = +#if FABLE_COMPILER + x <- x + i +#else + Interlocked.Add(&x, i) |> ignore +#endif + let a = Async.Parallel [ + async { add 1 } + async { add 2 } + ] + do! Async.Sleep 100 + equal 0 x + let! _ = a + equal 3 x + } + |> Async.StartImmediate -// let makeWork i = -// async { -// // check that the individual work items run sequentially and not interleaved -// _aggregate <- _aggregate + i -// let copyOfI = _aggregate -// do! Async.Sleep 100 -// equal copyOfI _aggregate -// do! Async.Sleep 100 -// equal copyOfI _aggregate -// return i -// } -// let works = [ for i in 1 .. 5 -> makeWork i ] -// let now = DateTimeOffset.Now -// let! result = Async.Sequential works -// let ``then`` = DateTimeOffset.Now -// let d = ``then`` - now -// // For some reason this sometimes fail in CI if d is exactly 1s so give it some breadth -// if d < TimeSpan.FromSeconds 0.9 then -// failwithf "expected sequential operations to take longer than 1 second, but took %0.00f" d.TotalSeconds -// result |> equal [| 1 .. 5 |] -// result |> Seq.sum |> equal _aggregate -// } -// |> Async.StartImmediate +[] +let ``Async.Sequential works`` () = + async { + let mutable _aggregate = 0 + + let makeWork i = + async { + // check that the individual work items run sequentially and not interleaved + _aggregate <- _aggregate + i + let copyOfI = _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + do! Async.Sleep 100 + equal copyOfI _aggregate + return i + } + let works = [ for i in 1 .. 5 -> makeWork i ] + let now = DateTimeOffset.Now + let! result = Async.Sequential works + let ``then`` = DateTimeOffset.Now + let d = ``then`` - now + // For some reason this sometimes fail in CI if d is exactly 1s so give it some breadth + if d < TimeSpan.FromSeconds 0.9 then + failwithf "expected sequential operations to take longer than 1 second, but took %0.00f" d.TotalSeconds + result |> equal [| 1 .. 5 |] + result |> Seq.sum |> equal _aggregate + } + |> Async.StartImmediate -// [] -// let ``Async.Sequential is lazy`` () = -// async { -// let mutable x = 0 -// let a = Async.Sequential [ -// async { x <- x + 1 } -// async { x <- x + 2 } -// ] -// do! Async.Sleep 100 -// equal 0 x -// let! _ = a -// equal 3 x -// } -// |> Async.StartImmediate +[] +let ``Async.Sequential is lazy`` () = + async { + let mutable x = 0 + let a = Async.Sequential [ + async { x <- x + 1 } + async { x <- x + 2 } + ] + do! Async.Sleep 100 + equal 0 x + let! _ = a + equal 3 x + } + |> Async.StartImmediate // #if FABLE_COMPILER // [] @@ -688,58 +688,58 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Nested failure propagates in async expressions`` () = -// async { -// let data = ref "" -// let f1 x = -// async { -// try -// failwith "1" -// return x -// with -// | e -> return! failwith ("2 " + e.Message) -// } -// let f2 x = -// async { -// try -// return! f1 x -// with -// | e -> return! failwith ("3 " + e.Message) -// } -// let f() = -// async { -// try -// let! y = f2 4 -// return () -// with -// | e -> data := e.Message -// } -// |> Async.StartImmediate -// f() -// do! Async.Sleep 100 -// equal "3 2 1" !data -// } -// |> Async.StartImmediate +[] +let ``Nested failure propagates in async expressions`` () = + async { + let data = ref "" + let f1 x = + async { + try + failwith "1" + return x + with + | e -> return! failwith ("2 " + e.Message) + } + let f2 x = + async { + try + return! f1 x + with + | e -> return! failwith ("3 " + e.Message) + } + let f() = + async { + try + let! y = f2 4 + return () + with + | e -> data := e.Message + } + |> Async.StartImmediate + f() + do! Async.Sleep 100 + equal "3 2 1" !data + } + |> Async.StartImmediate -// [] -// let ``Try _ finally _ expressions inside async expressions work`` () = -// async { -// let data = ref "" -// async { -// try data := !data + "1 " -// finally data := !data + "2 " -// } |> Async.StartImmediate -// async { -// try -// try failwith "boom!" -// finally data := !data + "3" -// with _ -> () -// } |> Async.StartImmediate -// do! Async.Sleep 100 -// equal "1 2 3" !data -// } -// |> Async.StartImmediate +[] +let ``Try _ finally _ expressions inside async expressions work`` () = + async { + let data = ref "" + async { + try data := !data + "1 " + finally data := !data + "2 " + } |> Async.StartImmediate + async { + try + try failwith "boom!" + finally data := !data + "3" + with _ -> () + } |> Async.StartImmediate + do! Async.Sleep 100 + equal "1 2 3" !data + } + |> Async.StartImmediate // [] // let ``Final statement inside async expressions can throw`` () = @@ -848,17 +848,17 @@ let ``Async.Sleep works`` () = // } // |> Async.StartImmediate -// [] -// let ``Unit arguments are erased`` () = // See #1832 -// let mutable token = 0 -// async { -// let! res = -// async.Return 5 -// |> asyncMap (fun x -> token <- x) -// equal 5 token -// res -// } -// |> Async.StartImmediate +[] +let ``Unit arguments are erased`` () = // See #1832 + let mutable token = 0 + async { + let! res = + async.Return 5 + |> asyncMap (fun x -> token <- x) + equal 5 token + res + } + |> Async.StartImmediate // [] // let ``Can use custom exceptions in async workflows #2396`` () = @@ -1066,22 +1066,32 @@ let ``try finally in an async block runs the finalizer on failure`` () = throwsAnyError (fun () -> comp |> Async.RunSynchronously) ran |> equal 1 -// Note this asserts only that the resource binds and the block returns. -// Dispose is not called for a `use` on the Rust target at all -- see the -// disabled cases in MiscTests.fs -- so asserting disposal here would pass on -// .NET and fail on Rust. -type AsyncDisposable() = +type AsyncDisposable(onDispose: unit -> unit) = interface System.IDisposable with - member _.Dispose() = () + member _.Dispose() = onDispose() [] -let ``use in an async block binds the resource`` () = +let ``use in an async block disposes the resource`` () = + let mutable isDisposed = false let comp = async { - use res = new AsyncDisposable() + use res = new AsyncDisposable(fun () -> isDisposed <- true) let! x = async { return 1 } return (if isNull (box res) then 0 else x) } comp |> Async.RunSynchronously |> equal 1 + isDisposed |> equal true + +[] +let ``use in an async block disposes the resource when the body fails`` () = + let mutable isDisposed = false + let comp = async { + use res = new AsyncDisposable(fun () -> isDisposed <- true) + let! _ = async { return 1 } + failwith "boom" + return 0 + } + throwsAnyError (fun () -> comp |> Async.RunSynchronously |> ignore) + isDisposed |> equal true [] let ``nested loops in an async block`` () = diff --git a/tests/Rust/tests/src/TaskTests.fs b/tests/Rust/tests/src/TaskTests.fs index 0b77a38245..a6ae7eb406 100644 --- a/tests/Rust/tests/src/TaskTests.fs +++ b/tests/Rust/tests/src/TaskTests.fs @@ -1,3 +1,4 @@ +[] module Fable.Tests.TaskTests open Util.Testing @@ -7,7 +8,7 @@ type DisposableAction(f) = interface System.IDisposable with member _.Dispose() = f () -let runSynchronously<'T> (tsk: Task<'T>) : 'T = +let inline runSynchronously<'T> (tsk: Task<'T>) : 'T = //tsk.GetAwaiter().GetResult() tsk.Result @@ -68,6 +69,25 @@ let ``Task exceptions are handled correctly`` () = result f true + f false |> equal 22 +[] +let ``Task failures propagate from already started tasks`` () = + let failing = + task { + failwith "boom" + return 0 + } + + let result = + task { + try + return! failing + with + | _ -> return 42 + } + |> runSynchronously + + result |> equal 42 + [] let ``Simple task is executed correctly`` () = let mutable result = false @@ -80,6 +100,26 @@ let ``Simple task is executed correctly`` () = |> runSynchronously result |> equal true +[] +let ``Task Combine runs computations in order`` () = + let mutable result = "" + let tsk = + task { + if true then + do! task { result <- result + "a" } + result <- result + "b" + } + tsk |> runSynchronously + result |> equal "ab" + +[] +let ``Task ReturnFrom returns inner task`` () = + let result = + task { return! task { return 42 } } + |> runSynchronously + + result |> equal 42 + [] let ``task use statements should dispose of resources when they go out of scope`` () = let mutable isDisposed = false @@ -98,6 +138,23 @@ let ``task use statements should dispose of resources when they go out of scope` step2ok <- isDisposed (step1ok && step2ok) |> equal true +[] +let ``task use statements dispose of resources when the body fails`` () = + let mutable isDisposed = false + let resource = + task { return new DisposableAction(fun () -> isDisposed <- true) } + + let failing = + task { + use! r = resource + ignore r + failwith "boom" + return 0 + } + + throwsAnyError (fun () -> failing.Result |> ignore) + isDisposed |> equal true + [] let ``Try ... with ... expressions inside async expressions work the same`` () = let mutable result = "" @@ -128,14 +185,14 @@ let ``Try ... with ... expressions inside async expressions work the same`` () = result |> equal "abcdef" -[] -let ``TaskCompletionSource is executed correctly`` () = - let x = - task { - let tcs = TaskCompletionSource() - tcs.SetResult 42 - return! tcs.Task - } - let result = - x |> runSynchronously - result |> equal 42 +// [] +// let ``TaskCompletionSource is executed correctly`` () = +// let x = +// task { +// let tcs = TaskCompletionSource() +// tcs.SetResult 42 +// return! tcs.Task +// } +// let result = +// x |> runSynchronously +// result |> equal 42 diff --git a/tests/Rust/tests/src/UnionTests.fs b/tests/Rust/tests/src/UnionTests.fs index 1f095aaf67..f9cb755fde 100644 --- a/tests/Rust/tests/src/UnionTests.fs +++ b/tests/Rust/tests/src/UnionTests.fs @@ -147,6 +147,24 @@ let ``Match with condition works`` () = b1 |> matchStringWhenStringNotHello |> equal "hello" b2 |> matchStringWhenStringNotHello |> equal "not hello" +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +[] +let ``Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + [] type ArcUnion = | ArcS of string diff --git a/tests/TypeScript/Fable.Tests.TypeScript.fsproj b/tests/TypeScript/Fable.Tests.TypeScript.fsproj index 580c200cc9..40ecc987de 100644 --- a/tests/TypeScript/Fable.Tests.TypeScript.fsproj +++ b/tests/TypeScript/Fable.Tests.TypeScript.fsproj @@ -33,6 +33,7 @@ + From 07991d4fa218619be1925411ef2d6a2e79ab2b46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:59:22 +0200 Subject: [PATCH 022/322] chore: Bump browserslist from 4.28.1 to 4.28.8 in /tests/React (#4937) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 51 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index fb39b0a4e3..2768bc2fbb 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -2188,9 +2188,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2209,9 +2209,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "funding": [ { "type": "opencollective", @@ -2228,11 +2228,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -2275,9 +2275,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "funding": [ { "type": "opencollective", @@ -2580,9 +2580,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", "license": "ISC" }, "node_modules/emittery": { @@ -4203,10 +4203,13 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "license": "MIT" + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5063,9 +5066,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", "funding": [ { "type": "opencollective", From 5444d6ffa768746776c39d869eb3ed836bc0ac91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:02:00 +0200 Subject: [PATCH 023/322] chore: Bump the all-pip group with 4 updates (#4932) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 8 +- uv.lock | 252 ++++++++++++++++++++++++------------------------- 2 files changed, 130 insertions(+), 130 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7e720e8d63..72d8353eb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.4,<0.17", - "maturin>=1.14.1,<2", + "ruff>=0.16.5,<0.17", + "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", - "pydantic>=2.13.4", + "pydantic>=2.13.5", "attrs>=26.1.0", "pyright>=1.1.411", - "ty>=0.0.74", + "ty>=0.0.75", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 9504d79325..a168a1df96 100644 --- a/uv.lock +++ b/uv.lock @@ -64,12 +64,12 @@ dev = [ dev = [ { name = "attrs", specifier = ">=26.1.0" }, { name = "dunamai", specifier = ">=1.26.2,<2" }, - { name = "maturin", specifier = ">=1.14.1,<2" }, - { name = "pydantic", specifier = ">=2.13.4" }, + { name = "maturin", specifier = ">=1.15.0,<2" }, + { name = "pydantic", specifier = ">=2.13.5" }, { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.4,<0.17" }, - { name = "ty", specifier = ">=0.0.74" }, + { name = "ruff", specifier = ">=0.16.5,<0.17" }, + { name = "ty", specifier = ">=0.0.75" }, ] [[package]] @@ -83,23 +83,23 @@ wheels = [ [[package]] name = "maturin" -version = "1.14.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, + { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, + { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, ] [[package]] @@ -131,7 +131,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -139,84 +139,84 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.4" +version = "0.16.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" }, - { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" }, - { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" }, - { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" }, - { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" }, - { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" }, - { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" }, - { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, + { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, + { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, + { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, ] [[package]] name = "ty" -version = "0.0.74" +version = "0.0.75" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/0f/c767853e88567a2ec7e996dd95e3105b1bc62c95d103689311ef0f4a603c/ty-0.0.74.tar.gz", hash = "sha256:da14344fc8625fc9ff359bafb856ad575636ea86d9bb6a629b146bff27b380e6", size = 6786318, upload-time = "2026-08-22T15:05:54.054Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d0/d0c96f898d6974a4a3569ab3efdf9512c04ad99f9203effb55f72497fe97/ty-0.0.75.tar.gz", hash = "sha256:4c5eead33dfbf6e2ebb4f400f74b51ffc9bab702a6f23ddb648a1cbb740387e3", size = 6868326, upload-time = "2026-08-26T20:23:40.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/95/6ded58bc97885c6d88fa1f9cd815031489200738f961cbf0466663213f80/ty-0.0.74-py3-none-linux_armv6l.whl", hash = "sha256:8969ef4e508debf00cf58f9ea85a539f799b1732c59cdfcecd037630b9755b30", size = 12790043, upload-time = "2026-08-22T15:05:05.015Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8a/5e323603b6ab8731144421877ee8a0f8ac5a5511e67857127caa09f6730e/ty-0.0.74-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51fb6cf5b98e1e1140825b2430943f78d744876a735231656eafbb4c3f7eca3c", size = 12371748, upload-time = "2026-08-22T15:05:08.609Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/ee72e08cb705281e8d8c42917dd577aa598a8a098008495fda5176ee3f6e/ty-0.0.74-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ebe60b1f0a948c793d6c77fc9e9ddda599e4f023c04ab16e8e03bcb428c3fa0", size = 12282403, upload-time = "2026-08-22T15:05:11.448Z" }, - { url = "https://files.pythonhosted.org/packages/da/b3/fd935b694ff68bc278af50f7ad04770b36ce6306399baef7e1847b553a9d/ty-0.0.74-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa97f407a695c890a53615966a663c7d2167e2cabe88db7ca1a24d62635cdfc8", size = 12345164, upload-time = "2026-08-22T15:05:14.19Z" }, - { url = "https://files.pythonhosted.org/packages/54/5c/5b5825268e029ebb164c909780103dbbae367f069801410068bf1cef29b3/ty-0.0.74-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:673ddb733d4a0db31385ba1ed9ff1f6bd9dc5565413ce57b1ca5ac4c7803da5d", size = 12556646, upload-time = "2026-08-22T15:05:16.994Z" }, - { url = "https://files.pythonhosted.org/packages/56/e7/515914e571d62ce0101744fed3f881936eeb1b30dc37beb72b4f7ca1e289/ty-0.0.74-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1028e7c6b4f6145e9704552f43a5fffdcd51b42263ffdcd9c9677762bc395a4a", size = 13311653, upload-time = "2026-08-22T15:05:20.254Z" }, - { url = "https://files.pythonhosted.org/packages/b0/07/d1452babb6f9266c2122cabc095180b70ed306fb770b2996753814d2237d/ty-0.0.74-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79841a8890493021fb308772474983316eb91f7b56cb227a6a05a06b262a36f0", size = 13768284, upload-time = "2026-08-22T15:05:23.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/60/8d4a2fc7842a47210a1cb0a16a187d9de39ad5d509a00fb74c1c073afcde/ty-0.0.74-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94859d321f3c6a6c8f7bfc3f40e8319cda7e6e012e613440f3dfd145d5010e2e", size = 13422306, upload-time = "2026-08-22T15:05:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/de/76/ebbc269a8c4efcc4d44624993bd188145f20d60ebda9680b15aaec42cc50/ty-0.0.74-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:970a8b2c09ff3be04c8a1c6767332d861be4fce85efe7bb205e4ade7c8655274", size = 12970637, upload-time = "2026-08-22T15:05:29.15Z" }, - { url = "https://files.pythonhosted.org/packages/9e/dd/b99f7236acbf856780ca1779a48143d2d9f2c24d7f531a0ce15a022b8a87/ty-0.0.74-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:795f763b3ded85574c2c2846a6fb8acf2aa76e9e83d761143e92b1f0c7ffa2cd", size = 13344891, upload-time = "2026-08-22T15:05:32.033Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d7/9ff7449a4c7e6428f2c6f298e74cf24b70668f29d45c249507a723ff3782/ty-0.0.74-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc086db5367d912c31c0cc872deb7387290e779a4b9b54fcb944673a7cd52c7b", size = 12395272, upload-time = "2026-08-22T15:05:34.702Z" }, - { url = "https://files.pythonhosted.org/packages/b1/dd/b23a5b6b35d37df89dc8dc5daa09efd9245a668b50c4c81c25de21567dc1/ty-0.0.74-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0314d7b391cf684e47c2fa093d2ce4c597cfc9b01d9a315fe204aed6359b271b", size = 12573079, upload-time = "2026-08-22T15:05:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/23/c5/ccba16239d6129533c8b3603458d0f4dd2ba69478e47059073968e74261d/ty-0.0.74-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4a45dd2e991e8bdae82ba78c8cd051b253f60bc71a6536598fa3ef580b4fc9b", size = 12832506, upload-time = "2026-08-22T15:05:40.505Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1c/2390912634dff4f341f97b397f2aee341ff062be0a66cda37d59375454f2/ty-0.0.74-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:210e2eac6b018fb934e2b8dac3956a0ba076a3fb1fa6f135058c825e5b759b81", size = 13154752, upload-time = "2026-08-22T15:05:43.355Z" }, - { url = "https://files.pythonhosted.org/packages/c4/33/a8c12188227e6f74f91853a7374e01ed81d6ad21c16c8b70e92dbebfe46a/ty-0.0.74-py3-none-win32.whl", hash = "sha256:db0bb6a8f098ef9bd1be861f73b4f7c0320d40d4c05c7ae0a8677d4e7aa4f6e5", size = 12130002, upload-time = "2026-08-22T15:05:46.058Z" }, - { url = "https://files.pythonhosted.org/packages/21/5c/064f28ccb9c234cfce5a2f7aa69a256663d5ae5bb0290b3a9706cc4d1e4c/ty-0.0.74-py3-none-win_amd64.whl", hash = "sha256:bebff181515255b3c78bd2e7693ae66fab6064ad4feea2065c68bc01022aa678", size = 12771435, upload-time = "2026-08-22T15:05:48.811Z" }, - { url = "https://files.pythonhosted.org/packages/fe/06/d6becdaca0315346c26b6df97cb0eafa81de4f870945d6989e88704374ed/ty-0.0.74-py3-none-win_arm64.whl", hash = "sha256:1a3469eaaf8c85b1c0a15bede25d36daea4b09fce1d913e965b24e24b3f1d6c6", size = 12558299, upload-time = "2026-08-22T15:05:51.543Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6c/b12d03505f17581f0cfa3c12273fe34c1d67b36dfda1bc561a6bdc16512b/ty-0.0.75-py3-none-linux_armv6l.whl", hash = "sha256:e5409f50db2246fd4bd039d93d261e0cfa1daa554a4fb77256f91072c570349a", size = 12972606, upload-time = "2026-08-26T20:22:59.716Z" }, + { url = "https://files.pythonhosted.org/packages/d1/aa/30f11eecd9215a9f87e8fe8baaf48f3ce905f5d75b8e4aac70f0091f130c/ty-0.0.75-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5e7b8b3472fb9bb2eeab314984b265df08a7a9d518867a9e6020eebc06570be2", size = 12527158, upload-time = "2026-08-26T20:23:02.767Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/7fd7001b0b5c6610bfbad7357e47d5fe6f82d4e84e94c53776a478f5e9f8/ty-0.0.75-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6ccf34169821fe0d23e3360deeef981d217963412f1d087b9bdd32ec57f7a57", size = 12400533, upload-time = "2026-08-26T20:23:04.965Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/1e284ea3d348d7be02f12d83bc22ed9ef193033f863f05b64db99027f141/ty-0.0.75-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842ebb41e9c6c334b40768704e20b1a69d5c6b08805b289d5e0e2565f49f2de1", size = 12420592, upload-time = "2026-08-26T20:23:07.427Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/d813271543370c47fd74b5118f2066ab32b0983e907b1821f3f9a6d0fa7f/ty-0.0.75-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7a5a723c5f1e0fab4ffbfe9bd95123a526ed48f206e5f25cb2161ca294007a", size = 12739219, upload-time = "2026-08-26T20:23:09.809Z" }, + { url = "https://files.pythonhosted.org/packages/31/5b/95b49cc5570fd92a7bf63732f649b31906158721e03c7fcb1b5be74ee3bf/ty-0.0.75-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54382f98e5da292fcd7104391afef5105c35bb2f312e29bea6f5fa419935255c", size = 13494046, upload-time = "2026-08-26T20:23:12.191Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0d/502d2dd68173cf020e1ad2bdbab9544c86776de0b0e2ed15f8c2fe006e3d/ty-0.0.75-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac13b180dc2aade2cd243f56b01650e78bf091a2e522ad3bc947245d7837c613", size = 13938899, upload-time = "2026-08-26T20:23:14.764Z" }, + { url = "https://files.pythonhosted.org/packages/20/5b/f3b12a25c07224456219fc2bd20db0ad7e40b304be0ff6aad728da0135f9/ty-0.0.75-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752df7951a443219d7f1ff817e3723c85d428565ff449e08a7a93ba821661526", size = 13656711, upload-time = "2026-08-26T20:23:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/f090ad306e2b15a07b332d647138c5264b89d9758855ecce8b8a10bcb153/ty-0.0.75-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd399feedf7cee816563c1baec45fc1c0b3c89f1ea42364920b688004b5b7da", size = 13093499, upload-time = "2026-08-26T20:23:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4b/f69b99aaaca0c7c65d5f114b186b26b21666f767b0c69eec99a2bdccc061/ty-0.0.75-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d7625f6f56c7dc1e873579fdc9e432a0e21e302afe847ab60704d2303442a92e", size = 13520580, upload-time = "2026-08-26T20:23:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e7/692c5f905c0345a15d2255fc74066d660f030254ae8dcdaf33f5a5c2f279/ty-0.0.75-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:89e7d527e95a2534b70cae29e94c104b84082760ea05927d23bb87280969c104", size = 12524095, upload-time = "2026-08-26T20:23:24.026Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9a/f42b12cf265ea95344bf554764c4791cfb273bdd628aadd7c209af7cadc3/ty-0.0.75-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0843f134440740706e01bee5f88f4cfc10e9b018bddb9e4ef4c12dc9fc0c9aef", size = 12756591, upload-time = "2026-08-26T20:23:26.126Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/9b180c133cb9cce48179a7d2bf9e1802d992aa8176a918e0e05205760b42/ty-0.0.75-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1bd0ec0e50ee1376875c88891efe6f549c3560fa5b2ddad79a425cd5a6218b9c", size = 12998754, upload-time = "2026-08-26T20:23:28.353Z" }, + { url = "https://files.pythonhosted.org/packages/39/f6/3c6ef5dd550103e29905121c67fb96a374564f31a2f44c6faa1af98c2d61/ty-0.0.75-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f9eafd561f90110d5e29f589ec3e956c4686e2f6631348d99276436f5cbe4d1", size = 13316474, upload-time = "2026-08-26T20:23:30.857Z" }, + { url = "https://files.pythonhosted.org/packages/bb/52/12776337874c821076bd5368e352ccd9e67174790abe3b856f749cb3524b/ty-0.0.75-py3-none-win32.whl", hash = "sha256:05063a6fafe2154b794a7f964515d148e51acd186d72d4a3acd347ee9fa19336", size = 12316315, upload-time = "2026-08-26T20:23:33.528Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/bb51e16af5c7138c9f52f8f3d0a401a371c6798d092e3b74926f186a9814/ty-0.0.75-py3-none-win_amd64.whl", hash = "sha256:81cf1ba5f6b7536ad56747865214255d9bc8e80533a689dbb9ddeaad464b09f1", size = 12917267, upload-time = "2026-08-26T20:23:35.978Z" }, + { url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" }, ] [[package]] From e74108f6469d7cfee739fe0c37b55ba7c1e32006 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:02:35 +0200 Subject: [PATCH 024/322] chore: Bump the all-pip group in /src/fable-library-py with 4 updates (#4933) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 430 +++++++++++++++++------------------ 1 file changed, 215 insertions(+), 215 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index 50fd21f86a..35a1de74c7 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,94 +61,94 @@ dev = [ [[package]] name = "hypothesis" -version = "6.165.10" +version = "6.167.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/74/e8d1f7080bdaa20daafc7e301075660576d6dae416d0e415df584f1517f0/hypothesis-6.167.0.tar.gz", hash = "sha256:23a60707a2bc5e0c59ef95e94264c4e8fd1c54b4ca1b95cbde74fb68b9c2eeef", size = 505791, upload-time = "2026-08-30T02:06:27.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/c1/9a9538e6d185baf5cc7f15bc3b76e08efbb3de4b3c782f234356449c0dd7/hypothesis-6.165.10-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f839d29d0cc12048cf073d88ca4fdf94d420bc2b8afd69641ff6d496422ccd4f", size = 783243, upload-time = "2026-08-16T22:55:44.058Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/b70d9d79e871a75cbdeccd9067f20ecdb9eb2a1dfa03c630be3ad13b8b30/hypothesis-6.165.10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e10858f57ed0e74baa04393845f469fe8ad502c16ece4499bef7700c575611bd", size = 778815, upload-time = "2026-08-16T22:55:46.948Z" }, - { url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" }, - { url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" }, - { url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/99/27450763853a034bca1574d3e0a315164b33ff49c3862df6872dda45e25e/hypothesis-6.165.10-cp310-abi3-win32.whl", hash = "sha256:b33dc30170a7402e03c180f2c5ef69dc077152f35b91621e9cebcde9c7d71746", size = 669039, upload-time = "2026-08-16T22:55:11.962Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fc/ff2988b72b5705ad9ca500444bf3f43e3c2f41edfa034bbfeb23b215791a/hypothesis-6.165.10-cp310-abi3-win_amd64.whl", hash = "sha256:e9f924aa610c0618445e1e8738c822c3190ce2a2699a0cb48ec3a351a96761f2", size = 675213, upload-time = "2026-08-16T22:55:01.697Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/821810d36f78d9d9421cd2c5d9d36983b45bb3575c3086276cc5c76f9f73/hypothesis-6.165.10-cp310-abi3-win_arm64.whl", hash = "sha256:1d305448e9bd8e2f4f3cea0eafd809efdaab4e998a0019bc615650c8463e42f1", size = 673537, upload-time = "2026-08-16T22:54:47.898Z" }, - { url = "https://files.pythonhosted.org/packages/26/61/5e89268ce03317fb9f82449a1b3efd9e599dee090288fd0cf7586c532fb1/hypothesis-6.165.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:73e6df02a6a62f8045b511c272f894d08e56d174504c793c9effcbc6778051a8", size = 783959, upload-time = "2026-08-16T22:55:29.078Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e9/f4e0832e81bb53b70cf1712e28c867db64245b32595b594217452e7dbd8d/hypothesis-6.165.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8b20f44773a9ab84400465e318712d8c2ca16418d35b9f80aa27fdf2d690ad10", size = 779684, upload-time = "2026-08-16T22:54:57.698Z" }, - { url = "https://files.pythonhosted.org/packages/77/de/ea072d3359d5678771bed407f80439e8ac7ca905d1031b0372f61bf5746e/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb8c7d05ea27a093a92b250904095d71d924b6b44e5795a415c1b20c265f0c65", size = 1108540, upload-time = "2026-08-16T22:55:03.282Z" }, - { url = "https://files.pythonhosted.org/packages/28/56/e7c395cdaa3d6c28b944c1c3c516dee50d2b7b3aeafa31874b57009ca51f/hypothesis-6.165.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dafd6d6ababfa3b14dd6e5f0378cb7c7d291895a31a40abcbb7cc74f396131", size = 1158089, upload-time = "2026-08-16T22:54:36.205Z" }, - { url = "https://files.pythonhosted.org/packages/b1/49/1c6d2c465b9c5fc3213f1be89be95ba53819ca0130248c484129ccfefb71/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa74636a49fc8077413ce8db3e85f1c4aff880788bb55bda56253118e036fe5b", size = 1284125, upload-time = "2026-08-16T22:54:37.727Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bc/7caf5ac3d0173bd57bd2a5ab854ca49a3664a4309257be1452f81025cc24/hypothesis-6.165.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2b112768cfb67f2b683e53e58c1a33d27811aacf60c942b8eb74635e469a73f6", size = 1325082, upload-time = "2026-08-16T22:54:38.952Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/9d844330f570d6a4f127a683eab1e78c8263e6e72b16189f3534fa6bf6de/hypothesis-6.165.10-cp310-cp310-win_amd64.whl", hash = "sha256:56cb8c9055e50545fe6e3e5a560ec25a724673b2e4051f3c24d44e3ebc35dd72", size = 675082, upload-time = "2026-08-16T22:54:29.872Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c2/b9546ace11f241c9c02d389f258cb80c14447a8c885771c9f1f0bc1d85ca/hypothesis-6.165.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:592107a0faf6c9c3a63a8dbf13dfb1cbda1cf599b0bc11c953221b00204b9ce1", size = 783716, upload-time = "2026-08-16T22:55:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/37/10/27c2fdd574fd798caf5e91eb51f7834b098f5d840ce733efb3fba79ef86e/hypothesis-6.165.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9180c362bde06fd05380298ded4e234fbc0d6ede0a864835bfd91c1e24283d5", size = 779507, upload-time = "2026-08-16T22:55:07.633Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/70bc23695f3783c4b0486b6cad47b08a20f791db4a3c1b25250add9659fa/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d623801ae3dcd97b77b983400ef3d48bf976648e4efff19929175322eaae074d", size = 1108406, upload-time = "2026-08-16T22:55:39.653Z" }, - { url = "https://files.pythonhosted.org/packages/71/4c/32e200bd7a352af4b7f4e3729aaa4cd002cb5fe8c4c6aef5599d0019f152/hypothesis-6.165.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f6236cfb90b7817bb1a6a087589ca4aa46d73170f0dd62963952ed5dadc589", size = 1157850, upload-time = "2026-08-16T22:55:24.394Z" }, - { url = "https://files.pythonhosted.org/packages/03/a5/8efc2a9a484822efc0d0da466f50094e0f2c068187faaf33831fc905873e/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad0764730e8e3421601c2cc7e1f054a9206c60ea0917165d8d9193dc453f34f1", size = 1283704, upload-time = "2026-08-16T22:54:27.279Z" }, - { url = "https://files.pythonhosted.org/packages/46/2a/90cc8d7463929c04786f29600de45f3227c12fa9bed1d5b7ce319b05e1c9/hypothesis-6.165.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:10d9a650a4666b0914831f769703d36140ed8039fd19bf9b71f615b8541eccf2", size = 1325077, upload-time = "2026-08-16T22:55:16.561Z" }, - { url = "https://files.pythonhosted.org/packages/82/ac/bc16faba4b42883e3d290bfaceff51e258b63fbbdf789bf9fe88df1ce537/hypothesis-6.165.10-cp311-cp311-win_amd64.whl", hash = "sha256:5671d2b2bf83bd4b6f02e55b32d432506eff5358c82f39b460a849ce19a2666e", size = 674920, upload-time = "2026-08-16T22:55:42.613Z" }, - { url = "https://files.pythonhosted.org/packages/e9/45/cde4f78afe2b9e29caecf38319eedc1deb76aebcacbdd128e03cbb2511c3/hypothesis-6.165.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:637445c1593a2a9d1024fda50082f07bb56baedda78d90a25f64b8111727ef94", size = 784835, upload-time = "2026-08-16T22:54:45.429Z" }, - { url = "https://files.pythonhosted.org/packages/7f/81/847f30b81cbfd07607296b3ce43067cf4f80799bd9244167f587de9c8081/hypothesis-6.165.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:713f4ce4e82c26b53031f139de959bc9e8b54d3995aa824b89bbdf8229df2a45", size = 776419, upload-time = "2026-08-16T22:55:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/04/66/4c71c5be7a49d84b8c3a9278c1807c4c81181ab5474beb27df9d4c40dc0e/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f", size = 1106830, upload-time = "2026-08-16T22:55:10.389Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/e2cbd2810e79f7a452a8ea9f6c6438ee718ce938d8cc12252cf0b36a81d3/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28", size = 1156952, upload-time = "2026-08-16T22:55:53.35Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8b/794ced36864825492ac3712d5acab5a257b4601e6a9dc2ccdd3937198f87/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81", size = 1280780, upload-time = "2026-08-16T22:54:34.983Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2d/550525442cdbcc2daf1f9bdd8ba35bcbde63db7c7a22f2ef137fbb49df2f/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f", size = 1324130, upload-time = "2026-08-16T22:55:48.659Z" }, - { url = "https://files.pythonhosted.org/packages/74/59/6caf69dd5fe03499ada94c9cec016bffcc164511c6b93fe680f01209b9ff/hypothesis-6.165.10-cp312-cp312-win_amd64.whl", hash = "sha256:3376f2594763aef14faa519b0fb27cae7ce9eeaab4c69efa07777499110306c9", size = 672337, upload-time = "2026-08-16T22:54:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/b1/fb/c82c5bd92864ffcf319772fedc8c9bf2dbe4ca14baa0fee6e49e67b5ba1c/hypothesis-6.165.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9d77c3be7b429875036ad0f0597c6e5cc6bb17894a4da005e3807de64d2673ad", size = 784726, upload-time = "2026-08-16T22:54:32.371Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b9/3d7acd08506da85557e65147b7f3fca8c47684e33be90bee0acb523920db/hypothesis-6.165.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:490c56b830772b0eca3b4b2cecb3741a1ed26b1d7206a279e1525dbf0aa95ee4", size = 776375, upload-time = "2026-08-16T22:55:13.303Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/662b94880f260b0a88de1fdcf60fc9984f6e2a796da549542adc10a7bc83/hypothesis-6.165.10-cp313-cp313-win_amd64.whl", hash = "sha256:c01dd04044c472e47193b54f68e84e08d6ebf4f29551885aa959b015f7cd9747", size = 672346, upload-time = "2026-08-16T22:56:03.792Z" }, - { url = "https://files.pythonhosted.org/packages/3f/77/55e020c9c576532ff7d20bf8b1dfa052ecbd5ada1949b02f76c44c966f7e/hypothesis-6.165.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9ccac776b2ca93b324806facd526ccb45da0fd035001c899a35b02c44431e209", size = 784833, upload-time = "2026-08-16T22:55:21.255Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f2/01da2adf829cf549eaddcabb8e8072077fb3d26da4275f4c1e89b2c0af74/hypothesis-6.165.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e5f95f7b622e4171096d92175dda0a560f0955ade9b8a3a07bdcf151f7359611", size = 776545, upload-time = "2026-08-16T22:56:10.159Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" }, - { url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" }, - { url = "https://files.pythonhosted.org/packages/48/86/9b4fb75f520a028edec50ffc904a94d724180395d71feb6d7a0ce7bb6f00/hypothesis-6.165.10-cp314-cp314-win_amd64.whl", hash = "sha256:d1ea02fa8ab3d33eb1125eade81f7136341eb429152c6dbe2ae6f8bc33b3fbdd", size = 672145, upload-time = "2026-08-16T22:54:24.831Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ba/f7bbaae0c789bab7ddb764d2056ee1a463cc95a8acbccc90d4184e48b242/hypothesis-6.165.10-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ed1a5891e59472884a03cb9875483e8fc131c80a275c60967f8afc5458a0c8ff", size = 783287, upload-time = "2026-08-16T22:54:23.751Z" }, - { url = "https://files.pythonhosted.org/packages/3a/83/01ef80772b4abd335c49405576dc503cede94fb5da30ba2643a119013aea/hypothesis-6.165.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:09772e328a26e50486ac572be34f9887f9aa185efe7ebb16bde4e8f6038db1f4", size = 774991, upload-time = "2026-08-16T22:55:25.987Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" }, - { url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" }, - { url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e7/b0a2fde7570c090a1b914026266a421c751ef10138fffe37fe0ef9e675c0/hypothesis-6.165.10-cp314-cp314t-win_amd64.whl", hash = "sha256:cc2da5aa4edf14743fa9257e5ba3513963999f01211635702479d8e92b8207c8", size = 672147, upload-time = "2026-08-16T22:55:27.527Z" }, - { url = "https://files.pythonhosted.org/packages/47/fd/985aa564d6ffd06483d45a62b40d319df0a703cd8bc1d041de17d102fbaa/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:eeab73050ea58c13dd56e329f594c1dfe32ebd7bb169bbdf4f8ceefbc31ec6b5", size = 782882, upload-time = "2026-08-16T22:55:37.93Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2c/6cc11151e450f72353a490940cd0db704680d07b78dc75dcc9f480e0d0e1/hypothesis-6.165.10-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:4c68e983d0007d014bb01ad4bcbba78bc432c73a1755ff36d5102ceefa18299a", size = 774584, upload-time = "2026-08-16T22:55:51.822Z" }, - { url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" }, - { url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" }, - { url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" }, - { url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" }, - { url = "https://files.pythonhosted.org/packages/55/63/ad5cc153dcc72ae5e7905fb9b3585f3e48ce892a2d6366f90163e867a69d/hypothesis-6.165.10-cp315-abi3.abi3t-win32.whl", hash = "sha256:c6559380469295c4009215fe1cab561301591a3bee2e2fb3f4f96d2273a3affc", size = 666038, upload-time = "2026-08-16T22:56:11.797Z" }, - { url = "https://files.pythonhosted.org/packages/80/32/b62307b73fbc99f0a4381d6f9456df76fbcbb7a27ef7256e26f0376f48ea/hypothesis-6.165.10-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:30797f20ca45e57f526d2df872f63ba453cb4e1091ad542184a7a951af8da79d", size = 671941, upload-time = "2026-08-16T22:55:00.235Z" }, - { url = "https://files.pythonhosted.org/packages/c2/dd/e0f98add0548ef73ea7afac45da1fb8efc854d7f9931db568754d0f963f3/hypothesis-6.165.10-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:c53e9b1c36350df9965ec44d6c0d4e0bbbb38f720dd2b0e1256dc6524d411015", size = 669931, upload-time = "2026-08-16T22:55:50.205Z" }, - { url = "https://files.pythonhosted.org/packages/0b/6a/880d6eeed5c451fb40a66733dadec4a5d498628a4a7f6a8a5f633f4c6dcb/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:34ee6402df6f31274d89119f1561b5f7489c97866afc5b7a3ed3a13d7e762802", size = 784644, upload-time = "2026-08-16T22:54:20.127Z" }, - { url = "https://files.pythonhosted.org/packages/27/e0/9e942bd3c3cf5ea0d5c0fd0905893bbfb6cefb7284c70fcc8033f8fdec38/hypothesis-6.165.10-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:277f41801e88dad2eba082f91a75632b7584ff64044ba2cf9dadf511b0d19cd0", size = 780515, upload-time = "2026-08-16T22:55:04.676Z" }, - { url = "https://files.pythonhosted.org/packages/19/32/f11a618415dc5fa9cdde41fea56c489f0814759527ae1ecd11a75a4558b9/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72df95fb1db41755b155c5f02106e0036a339250555c8d351d488704fd112cf9", size = 1109374, upload-time = "2026-08-16T22:56:00.241Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6f/db49b719842297c2b71e0d81e5b8967d31215fb7389421abcb465ce7ed3f/hypothesis-6.165.10-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20a02775eb3cf0ffb4f0219b6d7c1f240336663d4e5d7028675ec247c790c4", size = 1159092, upload-time = "2026-08-16T22:55:58.57Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/bf0bae84ba1cb3923d295973f1fe38ee867eaf90119e0d559116083be300/hypothesis-6.165.10-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1ec53f08732e3cfd0342cbbd75dbd1b193c8f19390660466e536a748bb81f757", size = 676045, upload-time = "2026-08-16T22:55:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/c3/81/36552b61148d1a8f7659ea5ff0a6ee9a430cbdcc370f686e797307c8634f/hypothesis-6.167.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dc30b21a0b9c0f1af43c6c2c53c009bbf08580e904f1f108bf4d13650a7c7784", size = 785398, upload-time = "2026-08-30T02:05:01.993Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/30558f078b9a3edf4cfb43c95ed7d71956e149a0c9cd84290c766e7ebb33/hypothesis-6.167.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:014b237fe33d9abf50a4bbdc1d9de04307a768bf0fee43422f541169b91eae53", size = 780984, upload-time = "2026-08-30T02:04:33.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/20/998a8b2c3a457f7303f3d96a7a7542fadc0addd8bc673a1ab2f93bc5a82f/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d3b8435c73a4502e57e757dd80eac73bbc0c9664b0149a9e31250d0711753c8", size = 1117782, upload-time = "2026-08-30T02:05:31.145Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6c/a97cb14e548b5b0d891ed4706d12445f896133df3198ade45e6d8a42b2d3/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34270e93976cf038852a990836a45f9df55883ee8d661131497ec5a1a067af37", size = 1141560, upload-time = "2026-08-30T02:04:16.144Z" }, + { url = "https://files.pythonhosted.org/packages/d6/0f/ebce0b534dff576d9de505084f9e940ea506567c2c0dd06bdb68dcbc2749/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95359d9dbe8057b4ed6ef51217540357303e2e558c61f1a2eaa641362648b7c", size = 1143210, upload-time = "2026-08-30T02:05:25.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0c/26aa55d3823c5a0f9c862794100f371210214663cd909ad297780543077e/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4346b4ab05c59613f828452bbf3c8165d31536141dfaece9ad22f9c56b23310d", size = 1163270, upload-time = "2026-08-30T02:05:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3d/f4a4ac1919ce29fbdcb4a5db3160690d7c35c47480c6e922386536930a43/hypothesis-6.167.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:31bdca9e569d28a2b17399163a150d7de51fe3d3161203087ee9ee34cba13390", size = 1122975, upload-time = "2026-08-30T02:05:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/6d/65/49633fe583d8470d40d19ad369990ae20068aa87abc185ebc2201631460b/hypothesis-6.167.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7b8658329582e1dddee4f0a563ac53c6ed7262a52e1eebbd72810a443ea6a19", size = 1153926, upload-time = "2026-08-30T02:04:40.492Z" }, + { url = "https://files.pythonhosted.org/packages/42/98/266c863d9bb2eb2a7541016d33e36f713a0c10ac39740ff603933830f632/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1f9c13b3680e37f30184ec9a9fc19d99eaf2278313252a6bc1c51a5fce5b2be4", size = 1293647, upload-time = "2026-08-30T02:04:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/88/0a/7645a13a3925898e0a46d8d07ae878777a84d5e3338a88320cab1e3b21ef/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5ad2bafeb2193981c5cc44490b4f329dd435b1c76c981844cd3549f031481bb2", size = 1419198, upload-time = "2026-08-30T02:06:05.889Z" }, + { url = "https://files.pythonhosted.org/packages/59/f5/f6fd5397ec7d4cbebcefa018680207dcc2f25d05b312fd4c66fc54b6436a/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:c27f11ee4a13960ea64f58b744ee134a50c160b83b8d3de24e7bb744f302fe0a", size = 1274832, upload-time = "2026-08-30T02:05:53.085Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/6cdc6e4e5f325f513f8d2672fbc0bb3c83cfbbac710080016db4729708c1/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:42be1bf942e4279892a9af46745363d053931daf4aef50634789a2301bcbed78", size = 1294087, upload-time = "2026-08-30T02:06:23.468Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d0/9ed25a5a6f4206e0c244f1c3949a2522c203eb624425101c30cf47e53452/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b7ecfb0d63107fad49e56b9ccdb64e76c37ca96559509c8114ed68a27affa5bf", size = 1329934, upload-time = "2026-08-30T02:05:03.719Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/9e0eac0dea23aab5b1331673fcbd1450650a791852d5ec59a6e0df48a606/hypothesis-6.167.0-cp310-abi3-win32.whl", hash = "sha256:5024a40cd2c82f7391499d5348c7cbf0360f6e6d3a116fb9020aa48f5a212483", size = 671853, upload-time = "2026-08-30T02:04:26.692Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/bf40b243c0a569026696173a817e7953067959e103551374d08aa2e8d48e/hypothesis-6.167.0-cp310-abi3-win_amd64.whl", hash = "sha256:f0301a5b9c04b6ec7b642e22dd581b64addfc1c07e9188e10394c94816c9b059", size = 678555, upload-time = "2026-08-30T02:06:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/3db36d9d71f291b96fcfb9ba68d696a046ac6723a69bad5e718c520e66dc/hypothesis-6.167.0-cp310-abi3-win_arm64.whl", hash = "sha256:cf4ee1dd5eb44f15c221b7bebab5c9f823e3593f6e09c3adb6ad8cc4af5f1eb0", size = 676571, upload-time = "2026-08-30T02:05:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f1/9beb0a073731080a61e4b3c6dde7e8993a02e782bcb2f4e648ae85f5a78d/hypothesis-6.167.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3796498cae6894e737d8569d8fe9d3be0650019ca6f378184d18f14cd522c6fd", size = 786102, upload-time = "2026-08-30T02:05:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/02/36/170fdd82d69ccb45be8b498ac5f1ba3d50c512d3d670fd3d47dcba82ed0e/hypothesis-6.167.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be0d0543e767ad1341736cc6e279a4e27de7f7ccbd62ca0fd0b4a5bd293f12d2", size = 781820, upload-time = "2026-08-30T02:04:51.055Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/9a60a32ad04f86296995e50e1eeb51654af6e9e19399e330e8be9bae9ba6/hypothesis-6.167.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c33f3d7c817e42d6f2c39199fdf94d54593b935df02e177ad26a2b5dbd64fbe3", size = 1118075, upload-time = "2026-08-30T02:05:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/f5/85/43ac592a84701751e7eb9f9b0c8d6659afa4e8a512e17e87f3f84ec039f5/hypothesis-6.167.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25434371146b9d59a552ff0bbaba5ec198cbbab5fe24448b20a1df6b220a660b", size = 1163859, upload-time = "2026-08-30T02:04:58.809Z" }, + { url = "https://files.pythonhosted.org/packages/85/06/d40a76e3a1295b1442c3e96ec7d422fb08a7342ccd7ab3f9faacf37b67de/hypothesis-6.167.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8decf66c7027dfa06b6e6bb9cd0ea0f19ae1fe12e9fa5c7b691e5d237c6622fd", size = 1294166, upload-time = "2026-08-30T02:04:11.982Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/6deaf923e4fb780719cabb80eef796294f3ec6905a701fe78e5c8ef912a4/hypothesis-6.167.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9315a9adfa59a1c0a9ae0d41e7fc6fdb826cd09a8e1aa3fd3e07a042fd75ffe", size = 1330619, upload-time = "2026-08-30T02:05:27.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/83/61895673efc7796316bf2f8087b0b12691798d16ab822ad151b42e290952/hypothesis-6.167.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b5a5f27ae48453ec2296f7a97600e965d4982bfcce772a66d12204672237848", size = 678501, upload-time = "2026-08-30T02:06:03.868Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/cad125cdd10c1eb16294ec0fd245d0ece2fd5fdffc9774499df6cd337551/hypothesis-6.167.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2149948b652a74795cd5b92b624624ff1228f29f2837b1f354a968178ac82860", size = 785868, upload-time = "2026-08-30T02:04:21.199Z" }, + { url = "https://files.pythonhosted.org/packages/9d/62/84f3ac632d2a0d1d9ccb08f75677b610f5b7bea99b75b2a03f92e626d3a3/hypothesis-6.167.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23110d0cafd4bb146b30903f1ce268dd98784ab3a49e7e420e67d0c94f2cac98", size = 781646, upload-time = "2026-08-30T02:05:43.783Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/5a8315cfc24edf7ae2f5706752f28d5b6ebc6f94f7d176bd27faa75a647f/hypothesis-6.167.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b44839487832addbc248998dcbef103237b03d2c59b5b10597139a784a8b69ca", size = 1117911, upload-time = "2026-08-30T02:05:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/cf696d3655e0a6ba6a29d4d63d5bbce2216e631db712bd9efdd79a17f949/hypothesis-6.167.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc84771e9f0d93c1bebf2ae8729bd967053ea44985758351a21fe628c9413ae", size = 1163715, upload-time = "2026-08-30T02:04:47.768Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bb/d020d9e6493408a986524f70c6b698d50e98d671e82cc9e3b34203d4699e/hypothesis-6.167.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1db3d15a8f77c347c711bd64b844c4610e97e41d27fb3b2c96d9611f60f5d7f4", size = 1294056, upload-time = "2026-08-30T02:04:43.556Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d8/d1db4fa76eb885fbf33972569e74bf88023b1eb08771c768179ebd09d3e8/hypothesis-6.167.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f38a2fcdc318795f22de72155c9991114f3fa041c1cbbadd82297cb40ca81f1b", size = 1330254, upload-time = "2026-08-30T02:04:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/2351c5a6c500ccc22f48d8162b3cd769a6c9def2488f019b84ae7d7d349c/hypothesis-6.167.0-cp311-cp311-win_amd64.whl", hash = "sha256:2dae6eeddabd8f420dea09e203b1c5945639a9b240cf8f38d63e954d5fa3b219", size = 678349, upload-time = "2026-08-30T02:05:47.455Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4d/dbe4cccac3fac6c338403dd5da42408779a44ada86a4db9810e329d0c9cd/hypothesis-6.167.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:42e4e0a97dba87463a7636c370d2b8dff690a74896d20ec016c5b2eba72b50e5", size = 786972, upload-time = "2026-08-30T02:04:34.662Z" }, + { url = "https://files.pythonhosted.org/packages/df/56/35aee232af8b772f0a37c35219f4d0f0695870427f7a6302d61ab4cd6dbb/hypothesis-6.167.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aac94c4414be605e6a2e6b95e0d0a976f2c09a6397faac3da6b2f9b1a7207e08", size = 778539, upload-time = "2026-08-30T02:05:57.277Z" }, + { url = "https://files.pythonhosted.org/packages/c7/71/c4c02d17d5404556c21180377bd50684acd42163216ea16930d9e95b6752/hypothesis-6.167.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220e68920f0da423c78aa0e06ee8461ad2eb964ff3fdb7cafb78754f3a992411", size = 1116634, upload-time = "2026-08-30T02:04:57.254Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c4/769c8db3bb95cf1835e816032ed2bc0c37a6a6d339b671cecaaa121f5f53/hypothesis-6.167.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b6369c7dc98ed0889121f5013145d5dc621977d0f594a8db737b8986a7e14a5", size = 1162621, upload-time = "2026-08-30T02:05:29.273Z" }, + { url = "https://files.pythonhosted.org/packages/63/3a/aaead27f746803574a7e82c8d6e2cdb15917ada21b989e5dfcd1eaade92c/hypothesis-6.167.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc7da295b1692701ca190c0e99c190edaf10a69260b200a95edf220fe35c0ed2", size = 1292346, upload-time = "2026-08-30T02:04:39.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b0/108d89dfab22011edf80b536fb66056439e83c77d1bb2e7513bf0128d55a/hypothesis-6.167.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2402fd4f170a4023d3ba5a20858a39e9e8fac105eced4eb619d7d11271be6f6b", size = 1329063, upload-time = "2026-08-30T02:05:20.655Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/c312388154cf737ce7416201339dc8324c9c6b0726927e3581b57d426739/hypothesis-6.167.0-cp312-cp312-win_amd64.whl", hash = "sha256:004ddebe0f85bc96f63b570659d0d37ff5ad4f4be7fa4b98f3313ce405019610", size = 675887, upload-time = "2026-08-30T02:04:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/94/1c/cf6854e55e7d35aa84de1c65a7f8bd8618dd15a8fea0c978a43c702852a1/hypothesis-6.167.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a4841ec07f0af94633e7f38b875cd472232c315cb58f2dc08f927e00084b159c", size = 786867, upload-time = "2026-08-30T02:04:14.94Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9d/6a5d6e744428d37bef067fd4d7767593bdd26a267c4c4103d7906dc9bfda/hypothesis-6.167.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07ea389b2351b1119142b2e736c5eb96aae361f988db3e1c36488c294b41552c", size = 778499, upload-time = "2026-08-30T02:05:13.474Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8c/beee8a3c429104e3ac7ccec5e0dc6d0cd0c70009fcf1a887c01ed47a4f73/hypothesis-6.167.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f060535e56a16029f7b3ef98da5a1a1e0816e76d9b4940c8b6237142d93401ed", size = 1116630, upload-time = "2026-08-30T02:04:27.847Z" }, + { url = "https://files.pythonhosted.org/packages/66/a7/ef7c79380525666204d77c8e8cf40be066acb04a1de56b838c0ec2e77e51/hypothesis-6.167.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfbfa3b76052270fb5b021c172506b7c72db3cabb1b57f96490f49e14006822", size = 1162486, upload-time = "2026-08-30T02:06:07.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/33/e017f20aec2a74e3d2032d8606c9b0e9729ee8c2fd580c4401f19216293e/hypothesis-6.167.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8789069253ef2e9091f1b3dbf364a658177de2711eac86626fa07851a37b7d4e", size = 1292150, upload-time = "2026-08-30T02:04:36.093Z" }, + { url = "https://files.pythonhosted.org/packages/32/4a/eee8b0a99646ac19a619fe8ac99befe3fbef610836345c4c9ac62e903249/hypothesis-6.167.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56e0073e8bac881ae7d7e3f6bed36dd766cc3941b9c2ec76dfdf5ae77cc6acf2", size = 1328888, upload-time = "2026-08-30T02:04:31.698Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/a52e0ff204fc5e9296bdab9eb7b27881ed7cbd53830aad22de35f6ba5dcc/hypothesis-6.167.0-cp313-cp313-win_amd64.whl", hash = "sha256:342576693423821587babc15064c4f0f13630624a5523db1963be9109686ef09", size = 675845, upload-time = "2026-08-30T02:05:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6a/53246d6641937c60b111be801e1d61867fe8f5c2fa6dce4da682f3c28f5a/hypothesis-6.167.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d9a01908ee8820800bd20b4ff289654650f4058583d3a80e58bae6c293ce78ca", size = 786974, upload-time = "2026-08-30T02:05:10.06Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/41a954266db4ede5632131488d0c571c062e5776f62032a88e5ac0872724/hypothesis-6.167.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9a5cad77635f77494b65b4869dcf0b6398dd0eececfe31a6868c8bf4a5b5bf28", size = 778645, upload-time = "2026-08-30T02:04:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c4/be221a5ac3c0cddd6a95dc20328dafe081fb1c97f45384960ff678e3778e/hypothesis-6.167.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c07b1cdf065c8a62e209951d26d8ce21f14e3b998755f1422c6e529dfd940e3f", size = 1116940, upload-time = "2026-08-30T02:05:36.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/af8a1a71f0c8517a957184b4a4af1450bbc307da17349d2762c06ca94dae/hypothesis-6.167.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ae0dc7cf9b0a20d39da08ec1fbe2c31c9811781582a4b7aea5bb9dd4509cef", size = 1162669, upload-time = "2026-08-30T02:05:55.178Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/c0ce1230f11f75408522dac6b2cac5cc942b9805838dc33339a61d316dd8/hypothesis-6.167.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b9542a2199016998600091ea4f7bf5239f368c7c965fd335abed87b2c4bd1de", size = 1292718, upload-time = "2026-08-30T02:05:05.312Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/7544ad88f2faf9f45ef13889b94dadcd1e4a0d97b5558e535330f1afc65b/hypothesis-6.167.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bdbef6d0b2e7ee36192a5e457ff0258d221ffd807bc1e10e9a25dae0dd06dcbf", size = 1329107, upload-time = "2026-08-30T02:05:24.103Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a83af13ba023c31e16ea328c8ddee2c241c1a680052ade3b191bbb9287f3/hypothesis-6.167.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:c628536f8a1b29ade49668388afd5978da8fbbd678bfe960e297525d853c3707", size = 617997, upload-time = "2026-08-30T02:05:22.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/560dbd7b95146add3d826d80d556ac8fc35ea01b4190f16795ba00f26f6c/hypothesis-6.167.0-cp314-cp314-win_amd64.whl", hash = "sha256:21039a571752c8faa69733b94f4fe04250a1dff759a87089bc3b423c1d216b4d", size = 675721, upload-time = "2026-08-30T02:05:49.26Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/7d2e3e67ab0862c978b5c8a591ae3dbae9eb1e8c1f625375e6beb513ec24/hypothesis-6.167.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:902f42b1f3c438278281266ba7f0c55202345287e8065e2eb98fabdcda8939eb", size = 785446, upload-time = "2026-08-30T02:06:21.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/73706a7291e03d798d686b9422c83fdf66fe955d35313143156dd05c3297/hypothesis-6.167.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3a235e3ef76f73e32922383bd507970cbc01fc08f17043d18a72508957af8f06", size = 777063, upload-time = "2026-08-30T02:05:06.954Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/ef7a3036abba07c3630cc60a8e7aa7ac619daa8d7dbb8e2689f4bde76dc7/hypothesis-6.167.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601965116dce06c1052ca0c89b69f0f1a49ad5b7d2da2b8352351c23296bd607", size = 1115266, upload-time = "2026-08-30T02:04:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/943686f9dfdd7ac6423dc998f9dbf055a656e13046d5ce0f99899a0ce72c/hypothesis-6.167.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578e5cefa5a293158342e3bbabc71e426ec29d5cdb41635789b55f4b48eb5f9e", size = 1161171, upload-time = "2026-08-30T02:04:24.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/96/5303010ae09a6d0480270145fc344ec0d331622132af3d2726b582a111a1/hypothesis-6.167.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7356b0c2cd1d0978b66c1e4b604a4a1221695b6396f4fd6f302a817ab0a09e59", size = 1290878, upload-time = "2026-08-30T02:05:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/f4/cfedcb75ecc35349ed77d7acbc7b0c9b68c6f3a406a5b2993c7d35ec5f6c/hypothesis-6.167.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a287d1cc06c4591ac2369997e400c99d31ad2debe52cc0f9bff12f216f80fa8", size = 1327922, upload-time = "2026-08-30T02:04:22.559Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/36bd9dd73e035c4ec92535ab77753beab20b52cdf1f9ca5873bc686ff2f9/hypothesis-6.167.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b2846cac259c96b81c6539f722c3b9fd2458a85e6273b7986dc08ee05c92318", size = 675646, upload-time = "2026-08-30T02:04:20.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/59/faa4ae00641d8b61475f1979b9cbdaeb3da321057f79120985529daa4afb/hypothesis-6.167.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:9a6896bf96592cadb3c268be95e47be8d2d093d3d9c3785fab77c13fc9b89143", size = 785024, upload-time = "2026-08-30T02:06:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/6383c81208c772090314c38ff29d798a3e4f864d19a2ac88025c629d77ad/hypothesis-6.167.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:cb75ab6af7c6e84717a718d208e8e59054c0eaa06cb290d8ec440a8b99d9fad4", size = 776692, upload-time = "2026-08-30T02:05:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/92665f9361c33a29996917725118303de85b4283ed4da63ea24428310e4a/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc721a2fc6511806b680b0cc618de9a29385c25125544065a0c11c11095629d6", size = 1115035, upload-time = "2026-08-30T02:04:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/82fddbfa3953ab65f95ecbb4ff8155176b5100ebfe53a129fee6f23cc4e6/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7117613a8b62d32cda04be4ee31ef8482bfa9d2ba31107f878b38f9cd4e440d", size = 1137728, upload-time = "2026-08-30T02:04:37.506Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/b1da21d3ecf8b7155ba5fcfd7d98e644801dc35b70b30b97a0b3d74e916f/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dee0107bb4a80dcd040f037c86f7fc96b03a70e8cf3e003761904ecb85db0b40", size = 1140371, upload-time = "2026-08-30T02:06:01.885Z" }, + { url = "https://files.pythonhosted.org/packages/ee/69/00325c56f3e9020c4383e54c379c977fafceb2395c8d0f33e008b5c0b2af/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7852a0469cba37e777c01cf6601146a48f76c8407c33e4ee093c5502aa571685", size = 1160531, upload-time = "2026-08-30T02:06:12.298Z" }, + { url = "https://files.pythonhosted.org/packages/84/10/e60eee7abdd435947c1d41f1e4f5b2e0096102ef11e1cfbad3ccd3037a53/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:b8730e59b9e0439e4cd40d6ef47c61fb5b9d09185ea56e9a91573b1cb0e1ccf9", size = 1120523, upload-time = "2026-08-30T02:05:16.953Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/6aff7cba6e3cc134ed1f911675bc775070584f6b4ae8e6e6c739cd5d68c0/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eea70c11aab935580afab3247e3a54c7eeceaf80d8452cf33e2a38015c2c139e", size = 1149342, upload-time = "2026-08-30T02:04:17.444Z" }, + { url = "https://files.pythonhosted.org/packages/72/ed/4d778a057d66fd121cc457d698c8f77690becf806736a0ce01106f5667af/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:7973858cf8d8546322efca0ecf3728f55495641da48fd70fd9e8d47fb4fa4935", size = 1290338, upload-time = "2026-08-30T02:06:16.628Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/3f90361451c3d4cab6b74aaefbe0c60aa1da40210e49152c40677a379adf/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:91dbd4d673d696075b4aa9506b0c94421756364edb46e38f63ab5a0fa33dd3e1", size = 1415663, upload-time = "2026-08-30T02:05:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/3b/01/2925ee0324e36cce6ca8449f52a371458a160ba18cfcafa3bb13b2ae8f72/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa55764b18f139e126d3360e6227d88ca25cfd110573f6051bab1235308098b1", size = 1272067, upload-time = "2026-08-30T02:04:52.395Z" }, + { url = "https://files.pythonhosted.org/packages/e5/49/e0ba54f0f64d4f4dd680d90033408ced3edfeb6c639d9665a2f9d342a546/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:534430c0dd66bee5d4f9263a34ee2877912e796e0ff2827b98d91837e071dd84", size = 1291474, upload-time = "2026-08-30T02:04:49.465Z" }, + { url = "https://files.pythonhosted.org/packages/8b/8e/46721d5228aef6e646ef62502aec263c6776bbca7b93a705ff83ab6789ba/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d0a82542a44bc192139c4d4e50800adf6e91b6629dc15ddd43ab870fe71a3654", size = 1327640, upload-time = "2026-08-30T02:04:30.429Z" }, + { url = "https://files.pythonhosted.org/packages/52/84/3fc3aad8bb9d891394bd0a4d0a4c5774a3d652ebb6e4a0b5533eef0c437f/hypothesis-6.167.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:29f6fa02f7e98ca1ea2b67083d3267a6de443a513694d5d30214aa62fc2c13cf", size = 669042, upload-time = "2026-08-30T02:05:38.093Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a6/ffaa4b788cee5ec624649e10640c1c9f6b9f60501fa67e58553ccd3a4683/hypothesis-6.167.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:3532de0fef4cb383e5959aca1b9d2af66804f661cac855e328bb8b00992e8800", size = 675350, upload-time = "2026-08-30T02:04:44.922Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/b0733d76531186c8726c73ef187717f995c6b191d9bbf9ba6b28c7a87946/hypothesis-6.167.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:064c6341919abb5776e6b51f701858670b584d1963923b336e78e6e1edf2152d", size = 673044, upload-time = "2026-08-30T02:05:39.834Z" }, + { url = "https://files.pythonhosted.org/packages/68/78/c67b8791234a007073d89563f54b19d7d13ba5dc68ec0927cb3a33932ef7/hypothesis-6.167.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b121253ff03e89656cab6e8ceea51c08215f56bc58c5d9eea35d250a885300c5", size = 786782, upload-time = "2026-08-30T02:05:32.824Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/66f968d8dbd781dbcb7ddd50af1565a2b7df3a17eed3056741ab76d7484c/hypothesis-6.167.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d717181cbc0daeb673a64b421276f21d1d619deb00877cc383601ac6dce9571", size = 782631, upload-time = "2026-08-30T02:06:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/4e79e57a200fc0ebd4c98673f11e4125842218b3d8de2fa88e00189424a2/hypothesis-6.167.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0e57b70dbd8b6a1fdd3791614104f7565a48928d6edde0a6c819f9778327fd", size = 1118596, upload-time = "2026-08-30T02:04:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c7/ded7f4975ccfcddb4b2b2ffca243de79d1abe8632ff22301d9c6ad819f74/hypothesis-6.167.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b156573357689e2056030383d9012408c898dbcf3b245e8c639e559494d6865", size = 1165329, upload-time = "2026-08-30T02:06:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/85/22a996622913321687cceef7e76741ee4f49d4e78faf2c2c9bb8f6d12ca6/hypothesis-6.167.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d02f1c7901c81009a8f24f03d717d9ef1d2ba2012e1b912a9a4f142ca428a40a", size = 679294, upload-time = "2026-08-30T02:04:53.982Z" }, ] [[package]] @@ -162,26 +162,26 @@ wheels = [ [[package]] name = "maturin" -version = "1.14.1" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, - { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, - { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, - { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, - { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, + { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, + { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, + { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, ] [[package]] @@ -212,17 +212,17 @@ wheels = [ ] [[package]] -name = "py-cpuinfo" -version = "9.0.0" +name = "py-cpuinfo2" +version = "10.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/97/a8b1ddada14c8280a047c0746f95cb05d94a31b1a331cea22bcdc2b2a82d/py_cpuinfo2-10.1.1.tar.gz", hash = "sha256:7861133863663f16e06eca63b12904ef100b5760415e92372dac0162799a4771", size = 100840, upload-time = "2026-03-25T21:49:40.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, + { url = "https://files.pythonhosted.org/packages/23/0a/ba69d2dde1ae12ef1d389ea5a216384c5ff6ef7a1e7a48d1e9b6686f6790/py_cpuinfo2-10.1.1-py3-none-any.whl", hash = "sha256:adc53396bfb206e6498d078ec2ab407f85799ecd819584ac36a8f80a2d4d762d", size = 23791, upload-time = "2026-03-25T21:49:39.574Z" }, ] [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -230,125 +230,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] @@ -393,15 +393,15 @@ wheels = [ [[package]] name = "pytest-benchmark" -version = "5.2.3" +version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "py-cpuinfo" }, + { name = "py-cpuinfo2" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/8f/83a15e40dbc34a580ee56eb56983cae5394c6e94d50cf28fe268e457be25/pytest_benchmark-5.3.0.tar.gz", hash = "sha256:358444d4e89be901ee2b6404fb043ac3d7684002ad7f3563cc153fca6339c965", size = 375410, upload-time = "2026-08-23T17:45:08.891Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, + { url = "https://files.pythonhosted.org/packages/eb/42/7e80f7cfa191e0a766d1de99b4661847415ad5db34f8209d81fd42175b59/pytest_benchmark-5.3.0-py3-none-any.whl", hash = "sha256:920ab1dfcffa718d49aa15ba144c7e357bda59216a0dc308016cc1c7236f719d", size = 48401, upload-time = "2026-08-23T17:45:07.094Z" }, ] [[package]] From af908e7f0dc38b5ccc1995d0d9a75eb752304493 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:03:30 +0200 Subject: [PATCH 025/322] chore: Bump the all-actions group with 2 updates (#4935) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 4592c2aa87..23b0becb9a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup-cli@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 151ac7ba16..334b20a88c 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@ea4b911d44a5336c74325a122a5fd9110b45ff06 # v0.87.4 + uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From c230b94e7cbf30bb47b4c109b5b063f9a5cd7516 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:04:26 +0200 Subject: [PATCH 026/322] chore: Bump the all-npm group in /tests/React with 3 updates (#4931) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 2161 ++++++++++++++++++--------------- tests/React/package.json | 2 +- 2 files changed, 1184 insertions(+), 979 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index 2768bc2fbb..a309f88fa2 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -9,7 +9,7 @@ "@babel/plugin-transform-react-jsx": "^7.29.7", "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", + "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.6", "jest": "^30", "jest-environment-jsdom": "^30", @@ -294,12 +294,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -450,12 +450,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -930,6 +930,85 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -956,16 +1035,16 @@ } }, "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.0.tgz", + "integrity": "sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", + "jest-message-util": "30.5.0", + "jest-util": "30.5.0", "slash": "^3.0.0" }, "engines": { @@ -973,17 +1052,17 @@ } }, "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.0.tgz", + "integrity": "sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/reporters": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", @@ -991,20 +1070,20 @@ "exit-x": "^0.2.2", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", + "jest-changed-files": "30.5.0", + "jest-config": "30.5.0", + "jest-haste-map": "30.5.0", + "jest-message-util": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-resolve-dependencies": "30.5.0", + "jest-runner": "30.5.0", + "jest-runtime": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", + "jest-watcher": "30.5.0", + "pretty-format": "30.5.0", "slash": "^3.0.0" }, "engines": { @@ -1032,62 +1111,62 @@ } }, "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz", + "integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", + "@jest/fake-timers": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-mock": "30.4.1" + "jest-mock": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", - "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.0.tgz", + "integrity": "sha512-825vac4Dmysbn2kU7VUQPoKuj/HNUpSTgv98KCByMOSPvHuj1/HpVZeLRsP/itDB2HFiDcoTUrsg8fSu3PxKBw==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/jsdom": "^21.1.7", + "@jest/environment": "30.5.0", + "@jest/fake-timers": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "jest-mock": "30.5.0", + "jest-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { + "@types/jsdom": "*", "canvas": "^3.0.0", "jsdom": "*" }, @@ -1098,110 +1177,124 @@ } }, "node_modules/@jest/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.0.tgz", + "integrity": "sha512-jEmgmgJEobJ3zEhDOGp1VAJ6JkoVelpS8uZ1ae1Ul/5lP78UKJKmmU0lciJwd6JdnqOXHaaS/QCKwbf1dHI9MA==", "license": "MIT", "dependencies": { - "expect": "30.4.1", - "jest-snapshot": "30.4.1" + "expect": "30.5.0", + "jest-snapshot": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.0.tgz", + "integrity": "sha512-5j0ztPxSy3McUJihjkDdCyCfjvT2hxykFTWsgEBZKB8qsw9ALdCiGTpTRH5gnf/d+qI4SflYUJ0dWNbzjQCWbA==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0" + "@jest/get-type": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.0.tgz", + "integrity": "sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "jest-message-util": "30.5.0", + "jest-mock": "30.5.0", + "jest-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", + "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", - "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.0.tgz", + "integrity": "sha512-h7eJx534czwL8lQMYB0hwLT4/HquO8EX/RtYL7RNUHyUyWWVciYjoudN4Ns5JmNvn2/jh0Vm9UstZjEzJJ5EsQ==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/types": "30.4.1", - "jest-mock": "30.4.1" + "@jest/environment": "30.5.0", + "@jest/expect": "30.5.0", + "@jest/types": "30.5.0", + "jest-mock": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", + "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.4.0" + "jest-regex-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/@jest/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, "node_modules/@jest/reporters": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.0.tgz", + "integrity": "sha512-FEAuusWm+PUOn9ydjaHhpOpyPmS6IbGE04HaKTuUI4zd8eqYZiXiNLoCsdCog/l2XnNj53E9pFy64UltcvfJKg==", "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", + "@jest/console": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", + "@jridgewell/trace-mapping": "^0.3.31", "@types/node": "*", "chalk": "^4.1.2", "collect-v8-coverage": "^1.0.2", "exit-x": "^0.2.2", - "glob": "^10.5.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", + "jest-message-util": "30.5.0", + "jest-util": "30.5.0", + "jest-worker": "30.5.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1219,9 +1312,9 @@ } }, "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -1231,12 +1324,12 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.0.tgz", + "integrity": "sha512-iWQtIsi2dRsO2oWzVceOeynuRJiYTW8gsDVp5wFQ02ipHluQsNgBpasWSHiawVxukIlsGLdCtCSbIEK7fPtpvQ==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1246,13 +1339,14 @@ } }, "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.0.tgz", + "integrity": "sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", + "@jridgewell/trace-mapping": "^0.3.31", "callsites": "^3.1.0", + "convert-source-map": "^2.0.0", "graceful-fs": "^4.2.11" }, "engines": { @@ -1260,13 +1354,13 @@ } }, "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.0.tgz", + "integrity": "sha512-9IlPqUzUMkVDmoDqSSrVVLroVotgN3hTUPWPwq24XWXhh1Zpg915RXZ5pgRJ/7j4YE/kng5nqjSn4p3S68SZJA==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.0", + "@jest/types": "30.5.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1275,14 +1369,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.0.tgz", + "integrity": "sha512-TXlvSDIVv482b83hD8A8WtxDJEmGF47f60W6jRXOQL0Isfs3hKtk4rZIm9R/jLzAibg8+c56y+Q00BQs8R7Xcg==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", + "@jest/test-result": "30.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", + "jest-haste-map": "30.5.0", "slash": "^3.0.0" }, "engines": { @@ -1290,22 +1384,22 @@ } }, "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.0.tgz", + "integrity": "sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", + "@jest/types": "30.5.0", + "@jridgewell/trace-mapping": "^0.3.31", + "babel-plugin-istanbul": "^8.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", + "jest-haste-map": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-util": "30.5.0", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -1315,13 +1409,13 @@ } }, "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.0.tgz", + "integrity": "sha512-s1N+79S4Yp9ZgklCauZXi+YPJdCdtStNYQT32stuD6EeQaIBGHoUfyj2P0YWy8RmuQfaJboO+ulxEvEheR/POQ==", "license": "MIT", "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -1384,136 +1478,431 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/pkgr" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", - "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11", - "vitest": ">= 0.32" + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "vitest": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "license": "MIT" + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" }, "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -1551,9 +1940,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "license": "MIT", "optional": true, "dependencies": { @@ -1700,9 +2089,9 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", - "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { @@ -1803,6 +2192,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1816,6 +2208,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1829,6 +2224,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1842,6 +2240,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1855,6 +2256,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1868,6 +2272,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1881,6 +2288,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1894,6 +2304,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1907,6 +2320,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1920,6 +2336,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2088,15 +2507,15 @@ } }, "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.0.tgz", + "integrity": "sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==", "license": "MIT", "dependencies": { - "@jest/transform": "30.4.1", + "@jest/transform": "30.5.0", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", + "babel-plugin-istanbul": "^8.0.0", + "babel-preset-jest": "30.5.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -2109,9 +2528,9 @@ } }, "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz", + "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==", "license": "BSD-3-Clause", "workspaces": [ "test/babel-8" @@ -2121,16 +2540,16 @@ "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.3", "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" + "test-exclude": "^7.0.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz", + "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==", "license": "MIT", "dependencies": { "@types/babel__core": "^7.20.5" @@ -2166,26 +2585,29 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz", + "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==", "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", + "babel-plugin-jest-hoist": "30.5.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0" } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/baseline-browser-mapping": { "version": "2.11.20", @@ -2200,12 +2622,15 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/browserslist": { @@ -2250,12 +2675,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2335,9 +2754,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", "license": "MIT" }, "node_modules/cliui": { @@ -2354,55 +2773,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2437,12 +2807,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2557,6 +2921,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2598,9 +2971,9 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/entities": { @@ -2624,6 +2997,12 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2678,12 +3057,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -2694,17 +3067,17 @@ } }, "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.0.tgz", + "integrity": "sha512-8fiMWcEjPU7B9nErC4FtFcCzf2tC6I75Qf7m8wzBAWC2taZmcno3yAFEjIQL34SwoGZNgPf63UDiJLyh4SMPaw==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "@jest/expect-utils": "30.5.0", + "@jest/get-type": "30.5.0", + "jest-matcher-utils": "30.5.0", + "jest-message-util": "30.5.0", + "jest-mock": "30.5.0", + "jest-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -2725,6 +3098,23 @@ "bser": "2.1.1" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2754,24 +3144,16 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/gensync": { @@ -2814,21 +3196,17 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2951,29 +3329,21 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2992,6 +3362,18 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3042,9 +3424,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3110,15 +3492,15 @@ } }, "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.0.tgz", + "integrity": "sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", + "@jest/core": "30.5.0", + "@jest/types": "30.5.0", "import-local": "^3.2.0", - "jest-cli": "30.4.2" + "jest-cli": "30.5.0" }, "bin": { "jest": "bin/jest.js" @@ -3136,13 +3518,13 @@ } }, "node_modules/jest-changed-files": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.0.tgz", + "integrity": "sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==", "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.4.1", + "jest-util": "30.5.0", "p-limit": "^3.1.0" }, "engines": { @@ -3150,28 +3532,28 @@ } }, "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.0.tgz", + "integrity": "sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/environment": "30.5.0", + "@jest/expect": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", + "jest-each": "30.5.0", + "jest-matcher-utils": "30.5.0", + "jest-message-util": "30.5.0", + "jest-runtime": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", "p-limit": "^3.1.0", - "pretty-format": "30.4.1", + "pretty-format": "30.5.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3193,35 +3575,35 @@ } }, "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.0.tgz", + "integrity": "sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/core": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-config": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", "yargs": "^17.7.2" }, "bin": { @@ -3240,32 +3622,32 @@ } }, "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.0.tgz", + "integrity": "sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/pattern": "30.5.0", + "@jest/test-sequencer": "30.5.0", + "@jest/types": "30.5.0", + "babel-jest": "30.5.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", - "glob": "^10.5.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-circus": "30.5.0", + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-runner": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", "parse-json": "^5.2.0", - "pretty-format": "30.4.1", + "pretty-format": "30.5.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3302,30 +3684,30 @@ } }, "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.0.tgz", + "integrity": "sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "pretty-format": "30.4.1" + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3344,24 +3726,24 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", + "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", "license": "MIT", "dependencies": { "detect-newline": "^3.1.0" @@ -3371,16 +3753,16 @@ } }, "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.0.tgz", + "integrity": "sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.0", "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" + "jest-util": "30.5.0", + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3399,28 +3781,29 @@ } }, "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.4.1.tgz", - "integrity": "sha512-o3nfaN4zej7qgk2X0j8Jhq/S9nAVKs2xK3QeQxeHVvpkEPxaA1yxDGydR+iVI7zPy7Cp62Aq2h3Ja46QvfWHGA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.0.tgz", + "integrity": "sha512-VVHN/G3zrxsQR398jvMalM76ALX6YBAftsYLCGtTeKRmz4f42YJAP05AGpk0VF5SLtoHdkfKunYGfnKPnmcEOA==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/environment-jsdom-abstract": "30.4.1", + "@jest/environment": "30.5.0", + "@jest/environment-jsdom-abstract": "30.5.0", + "@types/jsdom": "^21.1.7", "jsdom": "^26.1.0" }, "engines": { @@ -3436,55 +3819,53 @@ } }, "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.0.tgz", + "integrity": "sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", + "@jest/environment": "30.5.0", + "@jest/fake-timers": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" + "jest-mock": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.0.tgz", + "integrity": "sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", + "@parcel/watcher": "^2.6.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", + "fdir": "^6.5.0", "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" + "jest-regex-util": "30.5.0", + "jest-util": "30.5.0", + "jest-worker": "30.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" } }, "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.0.tgz", + "integrity": "sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.4.1" + "@jest/get-type": "30.5.0", + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3503,30 +3884,30 @@ } }, "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.0.tgz", + "integrity": "sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" + "jest-diff": "30.5.0", + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3545,34 +3926,34 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz", + "integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", + "jest-util": "30.5.0", "picomatch": "^4.0.3", - "pretty-format": "30.4.1", + "pretty-format": "30.5.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -3593,151 +3974,135 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.0.tgz", + "integrity": "sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/expect-utils": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", - "jest-util": "30.4.1" + "jest-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.0.tgz", + "integrity": "sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", + "jest-haste-map": "30.5.0", + "jest-util": "30.5.0", + "jest-validate": "30.5.0", "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "unrs-resolver": "^1.12.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.0.tgz", + "integrity": "sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==", "license": "MIT", "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" + "jest-regex-util": "30.5.0", + "jest-snapshot": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.0.tgz", + "integrity": "sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==", "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/console": "30.5.0", + "@jest/environment": "30.5.0", + "@jest/source-map": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", - "jest-util": "30.4.1", - "jest-watcher": "30.4.1", - "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" + "jest-docblock": "30.5.0", + "jest-environment-node": "30.5.0", + "jest-haste-map": "30.5.0", + "jest-leak-detector": "30.5.0", + "jest-message-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-runtime": "30.5.0", + "jest-util": "30.5.0", + "jest-watcher": "30.5.0", + "jest-worker": "30.5.0", + "p-limit": "^3.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.0.tgz", + "integrity": "sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==", + "license": "MIT", + "dependencies": { + "@jest/environment": "30.5.0", + "@jest/fake-timers": "30.5.0", + "@jest/globals": "30.5.0", + "@jest/source-map": "30.5.0", + "@jest/test-result": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", + "cjs-module-lexer": "^2.2.0", "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", + "es-module-lexer": "^2.1.0", + "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", + "jest-haste-map": "30.5.0", + "jest-message-util": "30.5.0", + "jest-mock": "30.5.0", + "jest-regex-util": "30.5.0", + "jest-resolve": "30.5.0", + "jest-snapshot": "30.5.0", + "jest-util": "30.5.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -3746,9 +4111,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", - "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.0.tgz", + "integrity": "sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -3756,20 +4121,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", + "@jest/expect-utils": "30.5.0", + "@jest/get-type": "30.5.0", + "@jest/snapshot-utils": "30.5.0", + "@jest/transform": "30.5.0", + "@jest/types": "30.5.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.4.1", + "expect": "30.5.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "pretty-format": "30.4.1", + "jest-diff": "30.5.0", + "jest-matcher-utils": "30.5.0", + "jest-message-util": "30.5.0", + "jest-util": "30.5.0", + "pretty-format": "30.5.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -3790,24 +4155,24 @@ } }, "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3817,12 +4182,12 @@ } }, "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.0.tgz", + "integrity": "sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==", "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -3834,17 +4199,17 @@ } }, "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.0.tgz", + "integrity": "sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==", "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", + "@jest/get-type": "30.5.0", + "@jest/types": "30.5.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.4.1" + "pretty-format": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3875,33 +4240,33 @@ } }, "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", + "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.0.tgz", + "integrity": "sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", + "@jest/test-result": "30.5.0", + "@jest/types": "30.5.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.4.1", + "jest-util": "30.5.0", "string-length": "^4.0.2" }, "engines": { @@ -3909,14 +4274,14 @@ } }, "node_modules/jest-worker": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", - "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.0.tgz", + "integrity": "sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==", "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.4.1", + "jest-util": "30.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -3946,9 +4311,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -4101,9 +4466,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4112,15 +4477,6 @@ "node": ">=10" } }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4146,15 +4502,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "license": "ISC", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4196,6 +4552,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4233,20 +4595,11 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.26.tgz", + "integrity": "sha512-LPmrA6J31t9ac9294T/WZV6AK5CUcFZ+VXQ7pNNc1+JAjeKthW7vqekOBu6pXzsxD7slwJ7M4XMhamojG3T4oA==", "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -4358,15 +4711,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4377,26 +4721,29 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/picocolors": { "version": "1.1.1", @@ -4522,20 +4869,6 @@ "license": "MIT", "peer": true }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", - "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -4643,16 +4976,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", @@ -4663,25 +4990,6 @@ "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4713,33 +5021,18 @@ "node": ">=10" } }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -4757,13 +5050,7 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { + "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -4775,21 +5062,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -4803,18 +5075,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -4876,12 +5136,12 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -4891,60 +5151,90 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=8" + "node": ">=18" } }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/tldts": { @@ -4965,12 +5255,6 @@ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "license": "BSD-3-Clause" - }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -5121,15 +5405,6 @@ "node": ">=18" } }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -5190,17 +5465,17 @@ } }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -5224,56 +5499,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/write-file-atomic": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", @@ -5287,10 +5512,22 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5339,9 +5576,9 @@ "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -5365,38 +5602,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/tests/React/package.json b/tests/React/package.json index d45fcc8a26..dd9e3f6c4c 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -11,7 +11,7 @@ "@babel/plugin-transform-react-jsx": "^7.29.7", "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.2", + "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.6", "jest": "^30", "jest-environment-jsdom": "^30", From 7f819375f98c4cd1bad9f3997e1cca8bde0a43fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:04:46 +0200 Subject: [PATCH 027/322] chore: Bump the all-nuget group with 1 update (#4934) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cea3f2d9f0..79213107ea 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "easybuild.shipit": { - "version": "3.0.1", + "version": "3.1.0", "commands": [ "shipit" ], From 8919ceb48f28d5b30f39ae367a2b183ef6895e8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:05:22 +0200 Subject: [PATCH 028/322] chore: Bump the all-npm group with 3 updates (#4930) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 224 +++++++++++++++++++++++----------------------- package.json | 6 +- 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/package-lock.json b/package-lock.json index 38749e54dd..dba2c13436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,12 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.2.0", + "@types/node": "^26.4.0", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.5", - "terser": "^5.50.0", + "rollup": "^4.63.1", + "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", - "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", - "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", - "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", - "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", - "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", - "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", - "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", - "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", - "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", - "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", - "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", - "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", - "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", - "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", - "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", - "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", - "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", - "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", - "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", - "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", - "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", - "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", - "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", - "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", - "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.62.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", - "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.62.5", - "@rollup/rollup-android-arm64": "4.62.5", - "@rollup/rollup-darwin-arm64": "4.62.5", - "@rollup/rollup-darwin-x64": "4.62.5", - "@rollup/rollup-freebsd-arm64": "4.62.5", - "@rollup/rollup-freebsd-x64": "4.62.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", - "@rollup/rollup-linux-arm-musleabihf": "4.62.5", - "@rollup/rollup-linux-arm64-gnu": "4.62.5", - "@rollup/rollup-linux-arm64-musl": "4.62.5", - "@rollup/rollup-linux-loong64-gnu": "4.62.5", - "@rollup/rollup-linux-loong64-musl": "4.62.5", - "@rollup/rollup-linux-ppc64-gnu": "4.62.5", - "@rollup/rollup-linux-ppc64-musl": "4.62.5", - "@rollup/rollup-linux-riscv64-gnu": "4.62.5", - "@rollup/rollup-linux-riscv64-musl": "4.62.5", - "@rollup/rollup-linux-s390x-gnu": "4.62.5", - "@rollup/rollup-linux-x64-gnu": "4.62.5", - "@rollup/rollup-linux-x64-musl": "4.62.5", - "@rollup/rollup-openbsd-x64": "4.62.5", - "@rollup/rollup-openharmony-arm64": "4.62.5", - "@rollup/rollup-win32-arm64-msvc": "4.62.5", - "@rollup/rollup-win32-ia32-msvc": "4.62.5", - "@rollup/rollup-win32-x64-gnu": "4.62.5", - "@rollup/rollup-win32-x64-msvc": "4.62.5", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" } }, @@ -2568,9 +2568,9 @@ } }, "node_modules/terser": { - "version": "5.50.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", - "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", diff --git a/package.json b/package.json index a35f6a4bae..6ed28a9f21 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,12 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.2.0", + "@types/node": "^26.4.0", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.62.5", - "terser": "^5.50.0", + "rollup": "^4.63.1", + "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { From b4c45a93385f249a8586789037b0af18005786e9 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 2 Sep 2026 23:18:14 +0200 Subject: [PATCH 029/322] fix(python): emit Python names for float32 specials (#4938) --- .../Python/Fable2Python.Transforms.fs | 4 --- .../Python/Fable2Python.Util.fs | 12 +++++-- src/Fable.Transforms/Python/PythonPrinter.fs | 8 +++++ .../fable_library/core/floats.pyi | 8 ++--- tests/Python/TestArithmetic.fs | 31 +++++++++++++++++++ 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index aac00e39b3..3ae486e5ee 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -516,10 +516,6 @@ let transformValue (com: IPythonCompiler) (ctx: Context) r value : Expression * | 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 -> Expression.floatConstant (float x, ?loc = r), [] diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index d71adf1a28..51a854e994 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -1037,10 +1037,16 @@ module Util = | _, (:? uint64 as i) when i = 64UL -> makeFieldGet cons "SIXTY_FOUR" | _ -> Expression.call (cons, [ value ], ?loc = r), [] - let makeFloat (com: IPythonCompiler) (ctx: Context) r _t floatName x = + let makeFloat (com: IPythonCompiler) (ctx: Context) r _t floatName (x: float) = let cons = libValue com ctx "core" floatName - let value = Expression.floatConstant (x, ?loc = r) - Expression.call (cons, [ value ], ?loc = r), [] + + match x with + | x when Double.IsPositiveInfinity x -> makeFieldGet cons "infinity" + | x when Double.IsNegativeInfinity x -> makeFieldGet cons "negative_infinity" + | x when Double.IsNaN x -> makeFieldGet cons "nan" + | _ -> + let value = Expression.floatConstant (x, ?loc = r) + Expression.call (cons, [ value ], ?loc = r), [] // --------------------------------------------------------------------------- // Int32 normalization diff --git a/src/Fable.Transforms/Python/PythonPrinter.fs b/src/Fable.Transforms/Python/PythonPrinter.fs index aa89f08ed4..234c149811 100644 --- a/src/Fable.Transforms/Python/PythonPrinter.fs +++ b/src/Fable.Transforms/Python/PythonPrinter.fs @@ -792,6 +792,14 @@ module PrinterExtensions = printer.Print("\"") printer.Print(Naming.escapeString (fun _ -> false) value) printer.Print("\"") + | Constant(value = FloatLiteral value) when Double.IsNaN value || Double.IsInfinity value -> + // .NET prints these as `Infinity`, `-Infinity` and `NaN`, which are not Python names + if Double.IsNaN value then + printer.Print("float(\"nan\")") + elif Double.IsPositiveInfinity value then + printer.Print("float(\"inf\")") + else + printer.Print("float(\"-inf\")") | Constant(value = FloatLiteral value) -> let value = string value printer.Print(value) diff --git a/src/fable-library-py/fable_library/core/floats.pyi b/src/fable-library-py/fable_library/core/floats.pyi index 0ff33160ad..be85e63968 100644 --- a/src/fable-library-py/fable_library/core/floats.pyi +++ b/src/fable-library-py/fable_library/core/floats.pyi @@ -110,10 +110,10 @@ def log10(x: float) -> float: ... def log2(x: float) -> float: ... def degrees(x: float) -> float: ... def radians(x: float) -> float: ... -def is_nan(x: float) -> bool: ... -def is_infinity(x: float) -> bool: ... -def is_positive_infinity(x: float) -> bool: ... -def is_negative_infinity(x: float) -> bool: ... +def is_nan(x: SupportsFloat) -> bool: ... +def is_infinity(x: SupportsFloat) -> bool: ... +def is_positive_infinity(x: SupportsFloat) -> bool: ... +def is_negative_infinity(x: SupportsFloat) -> bool: ... def floor(x: float) -> float: ... def ceil(x: float) -> float: ... def pow(x: float, y: float | FloatNumeric) -> float: ... diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 2cc90d4e6c..1bd98a9604 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -17,6 +17,10 @@ let notALiteral = 5 let checkTo3dp (expected: float) actual = floor (actual * 1000.) |> equal expected +let moduleSinglePositiveInfinity = Single.PositiveInfinity +let moduleSingleNegativeInfinity = Single.NegativeInfinity +let moduleSingleNaN = Single.NaN + [] let ``test Adding floats works`` () = 3.141 + 2.85 |> equal 5.991 @@ -750,6 +754,33 @@ let ``test sqrt matches .NET core implementation`` () = // let result = Single.Parse("10.5", culture) // float result |> equal 10.5 +[] +let ``test Single special values work`` () = + Single.IsPositiveInfinity Single.PositiveInfinity |> equal true + Single.IsNegativeInfinity Single.NegativeInfinity |> equal true + Single.IsInfinity Single.PositiveInfinity |> equal true + Single.IsInfinity Single.NegativeInfinity |> equal true + Single.IsNaN Single.NaN |> equal true + Single.PositiveInfinity > 0.0f |> equal true + Single.NegativeInfinity < 0.0f |> equal true + Single.PositiveInfinity |> equal infinityf + Single.NegativeInfinity |> equal -infinityf + Single.IsNaN nanf |> equal true + +[] +let ``test Single special values work as module bindings`` () = + Single.IsPositiveInfinity moduleSinglePositiveInfinity |> equal true + Single.IsNegativeInfinity moduleSingleNegativeInfinity |> equal true + Single.IsNaN moduleSingleNaN |> equal true + +[] +let ``test Double special values work`` () = + Double.IsPositiveInfinity Double.PositiveInfinity |> equal true + Double.IsNegativeInfinity Double.NegativeInfinity |> equal true + Double.IsNaN Double.NaN |> equal true + Double.PositiveInfinity |> equal infinity + Double.NegativeInfinity |> equal -infinity + [] let ``test acos works`` () = acos 0.25 |> checkTo3dp 1318. From 9cf5bead5dee81c0373ba9c4cc798ebbb8fa5914 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 2 Sep 2026 23:50:04 +0200 Subject: [PATCH 030/322] ci: cache NuGet packages and npm downloads (#4525) Co-authored-by: github-actions[bot] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/setup-dotnet/action.yml | 18 +++++++ .github/actions/setup-node/action.yml | 14 ++++++ .github/workflows/build.yml | 64 +++++++------------------ 3 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 .github/actions/setup-dotnet/action.yml create mode 100644 .github/actions/setup-node/action.yml diff --git a/.github/actions/setup-dotnet/action.yml b/.github/actions/setup-dotnet/action.yml new file mode 100644 index 0000000000..923cdc6ccd --- /dev/null +++ b/.github/actions/setup-dotnet/action.yml @@ -0,0 +1,18 @@ +name: Setup .NET +description: Install the .NET SDK pinned by global.json with a cached NuGet package directory. + +runs: + using: composite + steps: + - uses: actions/setup-dotnet@v6 + with: + global-json-file: global.json + cache: true + # Package versions also come from .props, which .fsproj files interpolate. + # restore writes *.nuget.g.props into obj/. + cache-dependency-path: | + **/*.fsproj + **/*.props + **/*.targets + .config/dotnet-tools.json + !**/obj/** diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml new file mode 100644 index 0000000000..85be3be357 --- /dev/null +++ b/.github/actions/setup-node/action.yml @@ -0,0 +1,14 @@ +name: Setup Node.js +description: Install the Node.js version pinned by package.json with a cached npm download directory. + +runs: + using: composite + steps: + - uses: actions/setup-node@v7 + with: + node-version-file: package.json + cache: npm + # build.sh also installs in src/fable-compiler-js, src/fable-standalone and tests/React. + cache-dependency-path: | + **/package-lock.json + !**/node_modules/** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc565590d9..5b604a5846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore tools run: dotnet tool restore @@ -46,9 +44,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore project run: dotnet restore Fable.sln @@ -68,14 +64,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests - JavaScript (linux) if: matrix.platform == 'ubuntu-latest' @@ -94,14 +86,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests - TypeScript run: ./build.sh test typescript @@ -118,14 +106,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests run: ./build.sh test integration @@ -139,14 +123,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Fable Tests run: ./build.sh test standalone @@ -166,9 +146,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 @@ -278,14 +256,10 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Node.js environment - uses: actions/setup-node@v7 - with: - node-version-file: package.json + uses: ./.github/actions/setup-node - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -307,9 +281,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Dart SDK uses: dart-lang/setup-dart@v1 @@ -326,9 +298,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Setup Erlang/OTP uses: erlef/setup-beam@v1 @@ -358,9 +328,7 @@ jobs: - uses: actions/checkout@v7.0.1 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - name: Restore tools run: dotnet tool restore From 042991572969e1864a160b0a0145b1d59bc77236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:07:19 -0700 Subject: [PATCH 031/322] fix(rust): Fix interface casting, move reflection into declarations (#4939) --- .github/workflows/build.yml | 2 +- src/Fable.Transforms/Rust/Fable2Rust.fs | 67 ++++++++++++++++--- src/Fable.Transforms/Rust/Replacements.fs | 28 ++++++-- src/fable-library-rust/Cargo.toml | 1 + src/fable-library-rust/src/Reflection.rs | 40 ++++++----- src/quicktest-rust/Cargo.toml | 8 +++ tests/Rust/Cargo.toml | 1 + tests/Rust/tests/src/InterfaceTests.fs | 17 +++++ tests/Rust/tests/src/RecordReflectionTests.fs | 2 +- tests/Rust/tests/src/RecordTests.fs | 15 +++++ 10 files changed, 146 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b604a5846..fa82bf0420 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - features: [",", "no_std", "threaded", "lrc_ptr"] + features: [",", "lrc_ptr", "no_std", "reflection", "threaded"] steps: - uses: actions/checkout@v7.0.1 diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 4e3c20f7a5..b830194915 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1642,6 +1642,14 @@ module Util = let ar = makeLibCall com ctx None "HashMap" "entries" [ expr ] makeLibCall com ctx None "Seq" "ofArray" [ ar ] + // Casting Enumerators to IDisposable + | Fable.Any, IDisposable when + (match nestedExpr.Type with + | Replacements.Util.IsEnumerator _ -> true + | _ -> false) + -> + expr + // Boxing reads a value through an owned Rust expression. Clone first so fields // read from reference-counted records are not moved out of their owner. | t, Fable.Any when isValueType com t || isWrappedType com t -> expr |> makeClone |> boxValue com ctx @@ -1664,6 +1672,11 @@ module Util = [ expr |> makeClone ] |> makeLibCall com ctx None "Native" boxMethod + | Fable.DeclaredType(entRef, genArgs), Fable.Any when + ctx.SkipRecordTypeRegistration && isReferenceClass com entRef + -> + [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_" + // unboxing obj back to a reference-typed record/union: downcast + re-wrap. | Fable.Any, Fable.DeclaredType(entRef, genArgs) when isReferenceRecordOrUnion com entRef -> let rawTy = transformEntityType com ctx entRef genArgs @@ -1679,6 +1692,12 @@ module Util = [ expr ] |> makeLibCall com ctx genArgsOpt "Native" unboxMethod + | Fable.Any, Fable.DeclaredType(entRef, genArgs) when + ctx.SkipRecordTypeRegistration && isReferenceClass com entRef + -> + let genArgsOpt = transformGenArgs com ctx [ Fable.DeclaredType(entRef, genArgs) ] + [ expr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox" + // casts to generic param | _, Fable.GenericParam(name, _isMeasure, _constraints) -> makeCall (name :: "from" :: []) None [ expr ] // e.g. T::from(value) @@ -1691,8 +1710,18 @@ module Util = Replacements.Util.IsEntity ("System.IObservable`1") (_, [ observableType ]) when eventType = observableType -> makeLibCall com ctx None "Event" "asObservable" [ expr ] + // casts from interface to object + | t, Fable.Any when isInterface com t -> [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_" + // casts from object to interface - | t1, t2 when not (isInterface com t1) && (isInterface com t2) -> makeInterfaceCast com ctx t2 expr + | t1, t2 when not (isInterface com t1) && (isInterface com t2) -> + match t1, nestedExpr with + | Fable.Any, Fable.ObjectExpr _ -> makeInterfaceCast com ctx t2 expr + | Fable.Any, _ -> + let genArgsOpt = transformGenArgs com ctx [ t2 ] + let boxedExpr = makeLibCall com ctx None "Native" "box_" [ expr ] + [ boxedExpr ] |> makeLibCall com ctx genArgsOpt "Native" "unbox" + | _ -> makeInterfaceCast com ctx t2 expr // casts from interface to interface | _, t when isInterface com t -> expr |> makeClone |> mkCastExpr ty //TODO: not working, implement @@ -2168,14 +2197,7 @@ module Util = let path = makeFullNamePath entName genArgsOpt let expr = mkStructExpr path fields // TODO: range let expr = expr |> maybeWrapSmartPtr com ctx ent - - if ctx.SkipRecordTypeRegistration || isFableLibrary com || not ent.IsFSharpRecord then - expr - else - let registrationCtx = { ctx with SkipRecordTypeRegistration = true } - let typ = Fable.DeclaredType(entRef, genArgs) - let registration = makeRecordTypeInfo com registrationCtx r entRef genArgs typ - [ registration |> mkSemiStmt; expr |> mkExprStmt ] |> mkStmtBlockExpr + expr let tryUseKnownUnionCaseNames fullName = match fullName with @@ -2246,12 +2268,13 @@ module Util = let fmt = makeFormatString parts makeFormatExpr com ctx fmt values - // Builds a rich reflection value for `typeof`. The emitted expression + // Builds a rich reflection value for a record declaration. The emitted expression // registers the record's field metadata + constructor/getter closures keyed by // its concrete TypeId and returns a boxed RecordTypeInfo (the runtime value that // a `System.Type` holds on the Rust target). This backs FSharpValue.MakeRecord, // FSharpValue.GetRecordFields/GetRecordField and FSharpType.GetRecordFields. let makeRecordTypeInfo (com: IRustCompiler) ctx r (entRef: Fable.EntityRef) genArgs (typ: Fable.Type) : Rust.Expr = + let ctx = { ctx with SkipRecordTypeRegistration = true } let ent = com.GetEntity(entRef) let idents = getEntityFieldsAsIdents com ent let objType = Fable.Any @@ -2311,10 +2334,26 @@ module Util = makeLibCall com ctx None "Reflection" "recordType" [ tidExpr; nameExpr; fieldNamesExpr; makeExpr; gettersExpr ] + let makeRecordTypeInfoItem (com: IRustCompiler) ctx (ent: Fable.Entity) genArgs = + let typ = Fable.DeclaredType(ent.Ref, genArgs) + + let body = + makeRecordTypeInfo com ctx None ent.Ref genArgs typ |> mkExprBlock |> Some + + let returnType = transformType com ctx Fable.MetaType |> mkFnRetTy + let fnDecl = mkFnDecl [] returnType + let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS body + + let attr = [ mkAttr "cfg" [ "feature = \"reflection\"" ] ] + mkFnAssocItem attr "__type_info__" fnKind |> mkPublicAssocItem + let makeTypeInfo (com: IRustCompiler) ctx r (typ: Fable.Type) : Rust.Expr = match typ with | Fable.DeclaredType(entRef, genArgs) when (com.GetEntity(entRef)).IsFSharpRecord -> - makeRecordTypeInfo com ctx r entRef genArgs typ + let entName = getEntityFullName com ctx entRef + let genArgsOpt = transformGenArgs com ctx genArgs + let callee = makeStaticCallPathExpr (entName + "::__type_info__") genArgsOpt None + mkCallExpr callee [] | _ -> // Non-record `System.Type` value: carry the concrete `TypeId`, boxed as // `LrcPtr` so it flows through the same `obj` slot as record type @@ -5845,6 +5884,12 @@ module Util = |> List.append (makeFSharpExceptionItems com ctx ent) |> List.append (makePrimaryConstructorItems com ctx ent classDecl) + let memberItems = + if ent.IsFSharpRecord then + makeRecordTypeInfoItem com ctx ent genArgs :: memberItems + else + memberItems + if List.isEmpty memberItems then [] else diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 347f42d82b..1165752f61 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -2436,9 +2436,9 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with // Erased operators - | "CheckThis", _, [ arg ] - | "UnboxFast", _, [ arg ] - | "UnboxGeneric", _, [ arg ] -> Some arg + | "CheckThis", _, [ arg ] -> Some arg + | "UnboxFast", _, [ arg ] -> Some arg + | "UnboxGeneric", _, [ arg ] -> TypeCast(arg, t) |> Some | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args | "GetString", _, [ ar; idx ] -> Helper.LibCall(com, "String", "getCharAt", t, args, ?loc = r) |> Some | "GetStringSlice", None, [ ar; lower; upper ] -> @@ -3654,8 +3654,28 @@ let fsharpType com methName (r: SourceLocation option) t (i: CallInfo) (args: Ex let fsharpValue com methName (r: SourceLocation option) t (i: CallInfo) (args: Expr list) = match methName with + | "GetRecordFields" -> + let args, argTypes = + match args with + | record :: rest -> + let rec getConcreteType (expr: Expr) = + match expr with + | TypeCast(inner, _) -> getConcreteType inner + | _ -> expr.Type + + let typeInfo = Fable.Value(Fable.TypeInfo(getConcreteType record, []), None) + + let argTypes = + match i.SignatureArgTypes with + | first :: restTypes -> first :: Fable.MetaType :: restTypes + | [] -> [] + + record :: typeInfo :: rest, argTypes + | _ -> args, i.SignatureArgTypes + + Helper.LibCall(com, "Reflection", "getRecordFields", t, args, argTypes, ?loc = r) + |> Some | "GetUnionFields" - | "GetRecordFields" | "GetRecordField" | "GetTupleFields" | "GetTupleField" diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index 992dafb80f..07618e33c8 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -14,6 +14,7 @@ guid = ["dep:uuid"] lrc_ptr = [] no_std = ["dep:hashbrown"] random = ["dep:rand", "dep:chrono"] +reflection = [] regexp = ["dep:regex"] static_do_bindings = ["dep:startup"] threaded = ["atomic", "dep:futures", "dep:futures-timer"] diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 3a95cf5ce6..350d1fabef 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -2,7 +2,7 @@ pub mod Reflection_ { pub use core::any::TypeId; // re-export use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; - use crate::Native_::{box_, Any, Func1, LrcPtr, Vec}; + use crate::Native_::{box_, box_lrc, Any, Func1, LrcPtr, Vec}; use crate::NativeArray_::{array_from, Array}; use crate::String_::{fromString, string}; @@ -145,7 +145,7 @@ pub mod Reflection_ { } // Registry mapping a record's *concrete* TypeId to its reflection info. - // Populated by `typeof` and generated record construction. This is what makes + // Populated by `typeof` and generated record reflection calls. This is what makes // value-based reflection (GetRecordFields(record)) possible: from a bare // boxed record we can recover its runtime TypeId and look the info up. // @@ -155,22 +155,22 @@ pub mod Reflection_ { // unaffected, as those carry the info in the `System.Type` value itself. #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] thread_local! { - static RECORD_REGISTRY: RefCell> = + static RECORD_REGISTRY: RefCell>> = RefCell::new(HashMap::new()); } #[cfg(all(not(feature = "no_std"), feature = "threaded"))] - static RECORD_REGISTRY: OnceLock>> = OnceLock::new(); + static RECORD_REGISTRY: OnceLock>>> = OnceLock::new(); #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - fn registry_insert(tid: TypeId, info: RecordTypeInfo) { + fn registry_insert(tid: TypeId, info: LrcPtr) { RECORD_REGISTRY.with(|r| { r.borrow_mut().insert(tid, info); }); } #[cfg(all(not(feature = "no_std"), feature = "threaded"))] - fn registry_insert(tid: TypeId, info: RecordTypeInfo) { + fn registry_insert(tid: TypeId, info: LrcPtr) { RECORD_REGISTRY .get_or_init(|| RwLock::new(HashMap::new())) .write() @@ -179,17 +179,17 @@ pub mod Reflection_ { } #[cfg(feature = "no_std")] - fn registry_insert(_tid: TypeId, _info: RecordTypeInfo) { + fn registry_insert(_tid: TypeId, _info: LrcPtr) { // no registry when no_std } #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - fn registry_get(tid: &TypeId) -> Option { + fn registry_get(tid: &TypeId) -> Option> { RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) } #[cfg(all(not(feature = "no_std"), feature = "threaded"))] - fn registry_get(tid: &TypeId) -> Option { + fn registry_get(tid: &TypeId) -> Option> { RECORD_REGISTRY .get_or_init(|| RwLock::new(HashMap::new())) .read() @@ -199,11 +199,11 @@ pub mod Reflection_ { } #[cfg(feature = "no_std")] - fn registry_get(_tid: &TypeId) -> Option { + fn registry_get(_tid: &TypeId) -> Option> { None // no registry when no_std } - // Emitted by generated `typeof`. Builds the field metadata, registers + // Emitted by the generated record declaration's reflection method. Builds the field metadata, registers // the type by its concrete TypeId, and returns the boxed RecordTypeInfo // (which is what a `System.Type` value holds on the Rust target). pub fn recordType( @@ -213,6 +213,10 @@ pub mod Reflection_ { make: Func1, obj>, getters: Array>, ) -> obj { + if let Some(typ) = registry_get(&tid) { + return box_lrc(typ); + } + let names: Vec = field_names.get().iter().cloned().collect(); let gets: Vec> = getters.get().iter().cloned().collect(); let fields_vec: Vec> = names @@ -220,15 +224,15 @@ pub mod Reflection_ { .zip(gets.into_iter()) .map(|(n, g)| LrcPtr::new(RecordFieldInfo { name: n, get: g })) .collect(); - let info = RecordTypeInfo { + let info = LrcPtr::new(RecordTypeInfo { tid, name, fields: array_from(fields_vec), make, - }; + }); register_type_name_value(tid, info.name.clone()); registry_insert(tid, info.clone()); - box_(info) + box_lrc(info) } fn type_info_of(typ: &obj) -> RecordTypeInfo { @@ -273,9 +277,9 @@ pub mod Reflection_ { } // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] - pub fn getRecordFields(record: obj, _flags: Option) -> Array { - let tid = object_type_id(&record); - let info = registry_get(&tid).expect("Record type not registered; evaluate typeof first"); + pub fn getRecordFields(record: obj, typ: obj, _flags: Option) -> Array { + let info = type_info_of(&typ); + registry_insert(info.tid, LrcPtr::new(info.clone())); let vals: Vec = info .fields .get() @@ -330,7 +334,7 @@ pub mod Reflection_ { pub fn getTypeFromObj(o: obj) -> obj { let tid = object_type_id(&o); match registry_get(&tid) { - Some(info) => box_(info), + Some(typ) => box_lrc(typ), None => box_(tid), } } diff --git a/src/quicktest-rust/Cargo.toml b/src/quicktest-rust/Cargo.toml index 8b9d3740d0..d10bb35820 100644 --- a/src/quicktest-rust/Cargo.toml +++ b/src/quicktest-rust/Cargo.toml @@ -3,5 +3,13 @@ name = "fable_quicktest_rust" version = "0.1.0" edition = "2024" +[features] +lrc_ptr = ["fable_library_rust/lrc_ptr"] +no_std = ["fable_library_rust/no_std"] +reflection = ["fable_library_rust/reflection"] +static_do_bindings = ["fable_library_rust/static_do_bindings"] +threaded = ["fable_library_rust/threaded"] +# default = ["threaded"] # Uncomment to default to "threaded" mode + [dependencies] fable_library_rust = { path = "../../temp/fable-library-rust" } diff --git a/tests/Rust/Cargo.toml b/tests/Rust/Cargo.toml index ee81e8cb6a..d30344b606 100644 --- a/tests/Rust/Cargo.toml +++ b/tests/Rust/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [features] lrc_ptr = ["fable_library_rust/lrc_ptr"] no_std = ["fable_library_rust/no_std"] +reflection = ["fable_library_rust/reflection"] static_do_bindings = ["fable_library_rust/static_do_bindings"] threaded = ["fable_library_rust/threaded"] # default = ["no_std"] # Uncomment to default to "no_std" mode diff --git a/tests/Rust/tests/src/InterfaceTests.fs b/tests/Rust/tests/src/InterfaceTests.fs index 9bbc2e4beb..10dda94d94 100644 --- a/tests/Rust/tests/src/InterfaceTests.fs +++ b/tests/Rust/tests/src/InterfaceTests.fs @@ -15,6 +15,16 @@ type Adder3 (m: int) = interface IHasAdd with member _.Add x y = x + y - m +type IHasValue = + abstract Value: int + +type ValueHolder(value: int) = + interface IHasValue with + member _.Value = value + +let castToIHasValue (value: obj) : IHasValue = + value :?> IHasValue + let addWithAdder (i: IHasAdd) = i.Add 3 4 @@ -40,6 +50,13 @@ let ``Class interface impl works`` () = let res = aCasted.Add 2 1 res |> equal 3 +[] +let ``Interface cast through obj works`` () = + let original = ValueHolder(42) :> IHasValue + let boxed: obj = original :> obj + let casted = castToIHasValue boxed + casted.Value |> equal 42 + [] let ``Class interface impl works II`` () = let a = Adder2(1) diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs index 206d3acc82..3bd04ac322 100644 --- a/tests/Rust/tests/src/RecordReflectionTests.fs +++ b/tests/Rust/tests/src/RecordReflectionTests.fs @@ -1,6 +1,6 @@ // Value-based reflection uses a std-only type registry (it uses thread_local), so // these tests do not apply to no_std builds. -[] +[] module Fable.Tests.RecordReflectionTests open Util.Testing diff --git a/tests/Rust/tests/src/RecordTests.fs b/tests/Rust/tests/src/RecordTests.fs index 068c6db9b9..ce0d9000ab 100644 --- a/tests/Rust/tests/src/RecordTests.fs +++ b/tests/Rust/tests/src/RecordTests.fs @@ -2,6 +2,21 @@ module Fable.Tests.RecordTests open Util.Testing +type Camera() = class end + +type Scene = { + Camera: Camera +} + +let getCamera (scene: Scene) = + scene.Camera + +[] +let ``Class-valued record fields compile`` () = + let scene = { Camera = Camera() } + let _camera = getCamera scene + () + type MyRecord = { a: int b: string From 0775c604d319d230796da0d47817428b1da8fece Mon Sep 17 00:00:00 2001 From: Florian Verdonck Date: Thu, 3 Sep 2026 18:10:20 +0200 Subject: [PATCH 032/322] fix: report Fable's own logs from the Fable.Compiler API (#4923) --- src/Fable.Cli/Main.fs | 34 +--- src/Fable.Compiler/Library.fs | 102 +++++++++--- src/Fable.Compiler/Library.fsi | 23 ++- .../Compiler/CodeServicesProject/.gitignore | 1 + .../CodeServicesProject.fsproj | 17 ++ .../Integration/Compiler/CodeServicesTests.fs | 157 ++++++++++++++++++ .../Compiler/Fable.Tests.Compiler.fsproj | 1 + tests/Integration/Compiler/Main.fs | 1 + 8 files changed, 283 insertions(+), 53 deletions(-) create mode 100644 tests/Integration/Compiler/CodeServicesProject/.gitignore create mode 100644 tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj create mode 100644 tests/Integration/Compiler/CodeServicesTests.fs diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 03597f767a..a616e2528f 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -12,6 +12,7 @@ open Fable open Fable.AST open Fable.Transforms open Fable.Transforms.State +open Fable.Compiler open Fable.Compiler.ProjectCracker open Fable.Compiler.Util @@ -88,35 +89,6 @@ module private Util = |> Seq.filter (fun log -> log.Severity = Severity.Error) |> Seq.iter (fun log -> Log.error (formatLog rootDir log)) - let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = - diagnostics - |> Array.map (fun er -> - let severity = - match er.Severity with - | FSharpDiagnosticSeverity.Hidden - | FSharpDiagnosticSeverity.Info -> Severity.Info - | FSharpDiagnosticSeverity.Warning -> Severity.Warning - | FSharpDiagnosticSeverity.Error -> Severity.Error - - let range = - SourceLocation.Create( - start = - { - line = er.StartLine - column = er.StartColumn + 1 - }, - ``end`` = - { - line = er.EndLine - column = er.EndColumn + 1 - } - ) - - let msg = $"%s{er.Message} (code %i{er.ErrorNumber})" - - LogEntry.Make(severity, msg, fileName = er.FileName, range = range, tag = "FSHARP") - ) - let getOutPath (cliArgs: CliArgs) pathResolver file = match cliArgs.CompilerOptions.Language with // For Python we must have an outDir since all compiled files must be inside the same subdir, so if `outDir` is not @@ -672,7 +644,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let state = { state with - FSharpLogs = getFSharpDiagnostics results.Diagnostics + FSharpLogs = CodeServices.getFSharpDiagnostics results.Diagnostics HasFSharpCompilationFinished = true } @@ -1590,7 +1562,7 @@ let private compilationCycle (state: State) (changes: ISet) = match result with | Some ex -> - getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir + CodeServices.getFSharpDiagnostics diagnostics |> logErrors cliArgs.RootDir Log.error (ex.Message) return 1 | None -> diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs index e7b6c7c6df..64d0f0ebda 100644 --- a/src/Fable.Compiler/Library.fs +++ b/src/Fable.Compiler/Library.fs @@ -17,12 +17,24 @@ type TypeCheckProjectResult = ProjectCheckResults: FSharpCheckProjectResults } +type FableASTResult = + { + /// The current file transformed into the Fable AST + FableAST: AST.Fable.File + /// Everything the compiler reported: the diagnostics of the F# project as checked up to + /// the current file, tagged "FSHARP", followed by the logs Fable raised while + /// transforming that file, tagged "FABLE". + Logs: LogEntry array + } + type CompileResult = { /// A map of absolute file path to transpiled JavaScript code CompiledFiles: Map - /// Diagnostics of the entire checked F# project - Diagnostics: FSharpDiagnostic array + /// Everything the compiler reported: the diagnostics of the entire checked F# project, + /// tagged "FSHARP", followed by the logs Fable raised while translating the compiled + /// files, tagged "FABLE". + Logs: LogEntry array } type BabelWriter @@ -94,6 +106,35 @@ type BabelWriter module CodeServices = + let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = + diagnostics + |> Array.map (fun er -> + let severity = + match er.Severity with + | FSharpDiagnosticSeverity.Hidden + | FSharpDiagnosticSeverity.Info -> Severity.Info + | FSharpDiagnosticSeverity.Warning -> Severity.Warning + | FSharpDiagnosticSeverity.Error -> Severity.Error + + let range = + AST.SourceLocation.Create( + start = + { + line = er.StartLine + column = er.StartColumn + 1 + }, + ``end`` = + { + line = er.EndLine + column = er.EndColumn + 1 + } + ) + + let msg = $"%s{er.Message} (code %i{er.ErrorNumber})" + + LogEntry.Make(severity, msg, fileName = er.FileName, range = range, tag = "FSHARP") + ) + let compileFileToJs (com: Compiler) (pathResolver: PathResolver) @@ -145,7 +186,7 @@ module CodeServices = (cliArgs: CliArgs) (crackerResponse: CrackerResponse) (currentFile: string) - : Async + : Async = async { let! assemblies = checker.GetImportedAssemblies() @@ -173,7 +214,7 @@ module CodeServices = let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, @@ -185,10 +226,14 @@ module CodeServices = // TODO: make it configurable if FableTransforms.transformFile is applied? let fableAST = - FSharp2Fable.Compiler.transformFile compiler - |> FableTransforms.transformFile compiler + FSharp2Fable.Compiler.transformFile (compiler :> Compiler) + |> FableTransforms.transformFile (compiler :> Compiler) - return fableAST + return + { + FableAST = fableAST + Logs = Array.append (getFSharpDiagnostics checkProjectResult.Diagnostics) compiler.Logs + } } let compileMultipleFilesToJavaScript @@ -214,13 +259,13 @@ module CodeServices = let opts = cliArgs.CompilerOptions - let! compiledFiles = + let! results = inputFiles |> Seq.map (fun currentFile -> async { let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, @@ -234,17 +279,28 @@ module CodeServices = Path.ChangeExtension(currentFile, cliArgs.CompilerOptions.FileExtension) let! js = - compileFileToJs compiler pathResolver outputPath cliArgs.CompilerOptions.FileExtension - - return currentFile, js + compileFileToJs + (compiler :> Compiler) + pathResolver + outputPath + cliArgs.CompilerOptions.FileExtension + + // `AddLog` is not thread-safe, so every file keeps its own compiler and + // the logs are gathered once all files are done. + return (currentFile, js), compiler.Logs } ) |> Async.Parallel + let compiledFiles, fableLogs = Array.unzip results + return { CompiledFiles = Map.ofArray compiledFiles - Diagnostics = typeCheckProjectResult.ProjectCheckResults.Diagnostics + Logs = + Array.append + (getFSharpDiagnostics typeCheckProjectResult.ProjectCheckResults.Diagnostics) + (Array.concat fableLogs) } } @@ -305,14 +361,14 @@ module CodeServices = let opts = cliArgs.CompilerOptions - let! compiledFiles = + let! results = dependentFiles |> Array.filter (fun filePath -> not (filePath.EndsWith(".fsi", StringComparison.Ordinal))) |> Array.map (fun currentFile -> async { let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let compiler: Compiler = + let compiler: CompilerImpl = CompilerImpl( currentFile, fableProj, @@ -325,16 +381,24 @@ module CodeServices = let outputPath = Path.ChangeExtension(currentFile, ".js") let! js = - compileFileToJs compiler pathResolver outputPath cliArgs.CompilerOptions.FileExtension - - return currentFile, js + compileFileToJs + (compiler :> Compiler) + pathResolver + outputPath + cliArgs.CompilerOptions.FileExtension + + // `AddLog` is not thread-safe, so every file keeps its own compiler and + // the logs are gathered once all files are done. + return (currentFile, js), compiler.Logs } ) |> Async.Parallel + let compiledFiles, fableLogs = Array.unzip results + return { CompiledFiles = Map.ofArray compiledFiles - Diagnostics = checkProjectResult.Diagnostics + Logs = Array.append (getFSharpDiagnostics checkProjectResult.Diagnostics) (Array.concat fableLogs) } } diff --git a/src/Fable.Compiler/Library.fsi b/src/Fable.Compiler/Library.fsi index ed852b8a28..fa54ce00ad 100644 --- a/src/Fable.Compiler/Library.fsi +++ b/src/Fable.Compiler/Library.fsi @@ -4,15 +4,28 @@ open FSharp.Compiler.SourceCodeServices open FSharp.Compiler.Diagnostics open FSharp.Compiler.CodeAnalysis open Fable +open Fable.Transforms.State open Fable.Compiler.Util open Fable.Compiler.ProjectCracker +type FableASTResult = + { + /// The current file transformed into the Fable AST + FableAST: AST.Fable.File + /// Everything the compiler reported: the diagnostics of the F# project as checked up to + /// the current file, tagged "FSHARP", followed by the logs Fable raised while + /// transforming that file, tagged "FABLE". + Logs: LogEntry array + } + type CompileResult = { /// A map of absolute file path to transpiled JavaScript CompiledFiles: Map - /// Diagnostics of the entire checked F# project - Diagnostics: FSharpDiagnostic array + /// Everything the compiler reported: the diagnostics of the entire checked F# project, + /// tagged "FSHARP", followed by the logs Fable raised while translating the compiled + /// files, tagged "FABLE". + Logs: LogEntry array } type TypeCheckProjectResult = @@ -24,6 +37,10 @@ type TypeCheckProjectResult = [] module CodeServices = + /// Convert the diagnostics of the F# compiler into log entries tagged "FSHARP". + /// The error number is folded into the message, as `Fable.Cli` prints it. + val getFSharpDiagnostics: diagnostics: FSharpDiagnostic array -> LogEntry array + /// Type check a project using the InteractiveChecker val typeCheckProject: sourceReader: SourceReader -> @@ -39,7 +56,7 @@ module CodeServices = cliArgs: CliArgs -> crackerResponse: CrackerResponse -> currentFile: string -> - Async + Async /// And compile multiple files of a project to JavaScript. /// The expected usage of this function is either every file in the project or only the user files. diff --git a/tests/Integration/Compiler/CodeServicesProject/.gitignore b/tests/Integration/Compiler/CodeServicesProject/.gitignore new file mode 100644 index 0000000000..19ee8b3cc9 --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesProject/.gitignore @@ -0,0 +1 @@ +Program.fs diff --git a/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj b/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj new file mode 100644 index 0000000000..ce84c87b1b --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesProject/CodeServicesProject.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Integration/Compiler/CodeServicesTests.fs b/tests/Integration/Compiler/CodeServicesTests.fs new file mode 100644 index 0000000000..3431f0a7aa --- /dev/null +++ b/tests/Integration/Compiler/CodeServicesTests.fs @@ -0,0 +1,157 @@ +module Fable.Tests.Compiler.CodeServicesApi + +open System +open Expecto +open Fable +open Fable.Compiler.Util +open Fable.Compiler.ProjectCracker +open Fable.Transforms.State +open FSharp.Compiler.SourceCodeServices + +/// Drives `Fable.Compiler.CodeServices` directly, the way a host embedding the compiler does, +/// instead of going through `Fable.Cli.Main`. +module private Fixture = + + let private projDir = + IO.Path.Join(__SOURCE_DIRECTORY__, "CodeServicesProject") |> Path.normalizeFullPath + + let private projFile = + IO.Path.Join(projDir, "CodeServicesProject.fsproj") |> Path.normalizeFullPath + + let private sourceFile = IO.Path.Join(projDir, "Program.fs") |> Path.normalizeFullPath + + let private cliArgs = + { + CliArgs.ProjectFile = projFile + // These tests only look at what the compiler reports, they never run the emitted + // JavaScript, so point at a fixed directory instead of depending on a fable-library + // build being present. + FableLibraryPath = Some(IO.Path.Join(projDir, "fable_modules", "fable-library-js")) + RootDir = projDir + Configuration = "Debug" + OutDir = None + IsWatch = false + Precompile = false + PrecompiledLib = None + PrintAst = false + SourceMaps = false + SourceMapsRoot = None + NoRestore = false + NoCache = false + NoGitignore = false + NoParallelTypeCheck = false + Exclude = [ "Fable.Core" ] + Replace = Map.empty + RunProcess = None + CompilerOptions = CompilerOptionsHelper.Make() + Verbosity = Verbosity.Silent + } + + /// Cracking is expensive and the project options never change, only the source does. + let private crackerResponse = + lazy + (let resolver: ProjectCrackerResolver = Fable.Compiler.MSBuildCrackerResolver() + CrackerOptions(cliArgs, false) |> getFullProjectOpts resolver) + + let private pathResolver = + { new PathResolver with + member _.TryPrecompiledOutPath(_sourceDir, _relativePath) = None + member _.GetOrAddDeduplicateTargetDir(_importDir, _addTargetDir) = "" + } + + /// Writes `source` as the single file of the project and hands back what the compiler needs. + /// NOTE: NOT threadsafe, that file is rewritten on every call. + let private prepare (source: string) = + IO.File.WriteAllText(sourceFile, source) + let crackerResponse = crackerResponse.Value + + let _, sourceReader = + crackerResponse.ProjectOptions.SourceFiles + |> Array.map Fable.Compiler.File + |> Fable.Compiler.File.MakeSourceReader + + let checker = InteractiveChecker.Create(crackerResponse.ProjectOptions) + crackerResponse, sourceReader, checker + + let compile (source: string) = + let crackerResponse, sourceReader, checker = prepare source + + async { + let! typeCheckResult = + Fable.Compiler.CodeServices.typeCheckProject sourceReader checker cliArgs crackerResponse + + return! + Fable.Compiler.CodeServices.compileMultipleFilesToJavaScript + pathResolver + cliArgs + crackerResponse + typeCheckResult + [ sourceFile ] + } + |> Async.RunSynchronously + + let compileToFableAST (source: string) = + let crackerResponse, sourceReader, checker = prepare source + + Fable.Compiler.CodeServices.compileFileToFableAST + sourceReader + checker + cliArgs + crackerResponse + sourceFile + |> Async.RunSynchronously + +/// `Async.RunSynchronously` type-checks fine but has no translation, so Fable reports it through +/// `addErrorAndReturnNull` and emits `return null`. +let private unsupportedByFable = + """module Program + +let blocking () = Async.RunSynchronously(async { return 42 }) +""" + +let private fsharpTypeError = + """module Program + +let wrong: int = "not an int" +""" + +let tests = + testList + "CodeServices" + [ + testCase "Fable errors are reported" + <| fun _ -> + let result = Fixture.compile unsupportedByFable + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FABLE" && log.Message.Contains "not supported by Fable") + "Fable should report that Async.RunSynchronously is not supported" + + testCase "F# errors are reported" + <| fun _ -> + let result = Fixture.compile fsharpTypeError + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FSHARP") + "The F# type error should be reported" + + testCase "Fable errors are reported when only transforming to the Fable AST" + <| fun _ -> + let result = Fixture.compileToFableAST unsupportedByFable + + let errors = + result.Logs |> Array.filter (fun log -> log.Severity = Severity.Error) + + Expect.exists + errors + (fun log -> log.Tag = "FABLE" && log.Message.Contains "not supported by Fable") + "Fable should report that Async.RunSynchronously is not supported" + ] diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index 49253cae8a..d25012ac7c 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -23,6 +23,7 @@ + diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index 18c778cfa4..d8fddd671f 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -6,6 +6,7 @@ open Expecto let allTests = [ CompilerMessages.tests + CodeServicesApi.tests AnonRecordInInterface.tests CompilerHelpers.tests Inflate.tests From 5afa2948edd221b64aa14553509ba96028d064c5 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 3 Sep 2026 20:05:43 +0200 Subject: [PATCH 033/322] fix(python): compare nested options of different depth (#4940) --- .../fable_library/core/option.pyi | 3 +- src/fable-library-py/src/options.rs | 17 ++++++- src/fable-library-py/tests/test_option.py | 46 +++++++++++++++++++ tests/Python/TestOption.fs | 32 +++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/fable-library-py/tests/test_option.py diff --git a/src/fable-library-py/fable_library/core/option.pyi b/src/fable-library-py/fable_library/core/option.pyi index b0ec30ff99..ef88a73827 100644 --- a/src/fable-library-py/fable_library/core/option.pyi +++ b/src/fable-library-py/fable_library/core/option.pyi @@ -5,7 +5,8 @@ from typing import overload from . import Array class SomeWrapper[T]: - value: T + @property + def value(self) -> T: ... def __init__(self, value: T) -> None: ... def __repr__(self) -> str: ... def __str__(self) -> str: ... diff --git a/src/fable-library-py/src/options.rs b/src/fable-library-py/src/options.rs index 9d59505f12..5121e8ce2a 100644 --- a/src/fable-library-py/src/options.rs +++ b/src/fable-library-py/src/options.rs @@ -67,9 +67,12 @@ impl SomeWrapper { op: CompareOp, py: Python<'py>, ) -> PyResult { + if other.is_none() { + return Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge)); + } + let other_value = extract_value(py, other)?; - let result = self.value.bind(py).rich_compare(other_value, op)?; - result.is_truthy() + compare_values(self.value.bind(py), other_value.bind(py), op) } pub fn __repr__(&self, py: Python<'_>) -> PyResult { @@ -167,6 +170,16 @@ impl SomeWrapper { } } +// A `None` argument here is the inner option's `None` case, not an absent value. +fn compare_values(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { + match (a.is_none(), b.is_none()) { + (true, true) => Ok(matches!(op, CompareOp::Eq | CompareOp::Le | CompareOp::Ge)), + (true, false) => Ok(matches!(op, CompareOp::Ne | CompareOp::Lt | CompareOp::Le)), + (false, true) => Ok(matches!(op, CompareOp::Ne | CompareOp::Gt | CompareOp::Ge)), + (false, false) => a.rich_compare(b, op)?.is_truthy(), + } +} + // Helper function to check if an object is a Some wrapper fn is_some_wrapper(py: Python<'_>, obj: &Bound<'_, PyAny>) -> bool { obj.is_instance(&py.get_type::()) diff --git a/src/fable-library-py/tests/test_option.py b/src/fable-library-py/tests/test_option.py new file mode 100644 index 0000000000..fc6323fb98 --- /dev/null +++ b/src/fable-library-py/tests/test_option.py @@ -0,0 +1,46 @@ +"""Tests for nested options, represented by `fable_library.core.option.SomeWrapper`. + +Only nesting materializes a wrapper: `Some 42` and `Some (Some 42)` are the bare value, +`Some None` is `SomeWrapper(None)`. Two options of the same F# type can therefore put a +wrapper next to `None` or next to a bare value. +""" + +from fable_library.option import some +from fable_library.util import compare, compare_arrays, equal_arrays, equals + + +def test_equality_between_different_nesting_depths() -> None: + assert (some(None) == some(some(None))) is False + assert (some(some(None)) == some(None)) is False + assert (some(None) != some(some(None))) is True + assert equals(some(None), some(some(None))) is False + + +def test_equality_between_equal_nesting_depths() -> None: + assert (some(None) == some(None)) is True + assert (some(some(None)) == some(some(None))) is True + assert equals(some(some(None)), some(some(None))) is True + + +def test_equality_against_none_and_bare_values() -> None: + assert (some(None) == None) is False # noqa: E711 + assert (None == some(None)) is False # noqa: E711 + assert (some(None) == 42) is False + assert (42 == some(None)) is False + + +def test_equality_is_inherited_by_containers() -> None: + assert equal_arrays([some(None)], [some(some(None))]) is False + assert equal_arrays([some(None)], [some(None)]) is True + assert compare_arrays([some(None)], [some(some(None))]) == -1 + + +def test_comparison_orders_none_before_some() -> None: + assert compare(some(None), some(some(None))) == -1 + assert compare(some(some(None)), some(None)) == 1 + assert compare(some(None), some(None)) == 0 + assert compare(None, some(None)) == -1 + assert compare(some(None), None) == 1 + # `int option option`: `Some None` against `Some (Some 42)` + assert compare(some(None), 42) == -1 + assert compare(42, some(None)) == 1 diff --git a/tests/Python/TestOption.fs b/tests/Python/TestOption.fs index f776bdb992..f15d8b3745 100644 --- a/tests/Python/TestOption.fs +++ b/tests/Python/TestOption.fs @@ -191,6 +191,38 @@ let ``test Option.map/map2/map3/bind/filter with nested options work`` () = Some(Some 5) |> Option.bind (fun inner -> Some inner) |> equal (Some(Some 5)) Some(Some 5) |> Option.filter Option.isSome |> equal (Some(Some 5)) +[] +let ``test equality on nested options of different depth works`` () = + let a: int option option option = Some None + let b: int option option option = Some(Some None) + (a = b) |> equal false + (b = a) |> equal false + (a = a) |> equal true + (b = b) |> equal true + notEqual a b + let ra: Result = Ok a + let rb: Result = Ok b + (ra = rb) |> equal false + notEqual ra rb + let c: int option option = Some None + let d: int option option = Some(Some 42) + (c = d) |> equal false + (d = c) |> equal false + +[] +let ``test comparison on nested options of different depth works`` () = + let a: int option option option = Some None + let b: int option option option = Some(Some None) + compare a b |> equal -1 + compare b a |> equal 1 + compare a a |> equal 0 + compare (None: int option option option) a |> equal -1 + compare a (None: int option option option) |> equal 1 + let c: int option option = Some None + let d: int option option = Some(Some 42) + compare c d |> equal -1 + compare d c |> equal 1 + [] let ``test ValueOption.map/map2/map3/bind/filter/defaultWith/orElseWith work`` () = ValueSome 5 |> ValueOption.map (fun x -> x + 1) |> equal (ValueSome 6) From bf694309ae5dbc6bb546d9acc4d091f5e35fe330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:24:46 -0700 Subject: [PATCH 034/322] fix(rust): Support reflection for no_std (#4941) --- src/fable-library-rust/src/Reflection.rs | 122 ++++++++---------- tests/Rust/tests/src/RecordReflectionTests.fs | 5 +- 2 files changed, 53 insertions(+), 74 deletions(-) diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 350d1fabef..5efba43584 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -2,15 +2,15 @@ pub mod Reflection_ { pub use core::any::TypeId; // re-export use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; - use crate::Native_::{box_, box_lrc, Any, Func1, LrcPtr, Vec}; use crate::NativeArray_::{array_from, Array}; - use crate::String_::{fromString, string}; + use crate::Native_::{box_, box_lrc, Any, Func1, LrcPtr, MutCell, OnceInit, Vec}; + use crate::String_::string; - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - use core::cell::RefCell; + #[cfg(feature = "no_std")] + use hashbrown as collections; #[cfg(not(feature = "no_std"))] - use std::collections::HashMap; - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + use std::collections; + #[cfg(feature = "threaded")] use std::sync::{OnceLock, RwLock}; pub fn name() -> string { @@ -31,55 +31,50 @@ pub mod Reflection_ { (***value).type_id() } - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - thread_local! { - static TYPE_NAME_REGISTRY: RefCell> = - RefCell::new(HashMap::new()); - } + #[cfg(not(feature = "threaded"))] + static TYPE_NAME_REGISTRY: OnceInit>> = + OnceInit::new(); - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] - static TYPE_NAME_REGISTRY: OnceLock>> = OnceLock::new(); + #[cfg(feature = "threaded")] + static TYPE_NAME_REGISTRY: OnceLock>> = + OnceLock::new(); - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + #[cfg(not(feature = "threaded"))] fn register_type_name_value(tid: TypeId, name: string) { - TYPE_NAME_REGISTRY.with(|r| { - r.borrow_mut().insert(tid, name); - }); + TYPE_NAME_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get_mut() + .insert(tid, name); } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + #[cfg(feature = "threaded")] fn register_type_name_value(tid: TypeId, name: string) { TYPE_NAME_REGISTRY - .get_or_init(|| RwLock::new(HashMap::new())) + .get_or_init(|| RwLock::new(collections::HashMap::new())) .write() .unwrap() .insert(tid, name); } - #[cfg(feature = "no_std")] - fn register_type_name_value(_tid: TypeId, _name: string) {} - - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + #[cfg(not(feature = "threaded"))] fn registered_type_name(tid: &TypeId) -> Option { - TYPE_NAME_REGISTRY.with(|r| r.borrow().get(tid).cloned()) + TYPE_NAME_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get() + .get(tid) + .cloned() } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + #[cfg(feature = "threaded")] fn registered_type_name(tid: &TypeId) -> Option { TYPE_NAME_REGISTRY - .get_or_init(|| RwLock::new(HashMap::new())) + .get_or_init(|| RwLock::new(collections::HashMap::new())) .read() .unwrap() .get(tid) .cloned() } - #[cfg(feature = "no_std")] - fn registered_type_name(_tid: &TypeId) -> Option { - None - } - - #[cfg(not(feature = "no_std"))] fn canonical_type_name() -> string { let rust_name = core::any::type_name::(); let name = if TypeId::of::() == TypeId::of::() { @@ -101,22 +96,13 @@ pub mod Reflection_ { _ => rust_name, } }; - fromString(name.to_string()) - } - - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - pub fn register_type_name() { - register_type_name_value(TypeId::of::(), canonical_type_name::()); + string(name) } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] pub fn register_type_name() { register_type_name_value(TypeId::of::(), canonical_type_name::()); } - #[cfg(feature = "no_std")] - pub fn register_type_name() {} - // Compile-time helper emitted by `typeof` to obtain a concrete type id. pub fn type_id() -> TypeId { register_type_name::(); @@ -149,60 +135,54 @@ pub mod Reflection_ { // value-based reflection (GetRecordFields(record)) possible: from a bare // boxed record we can recover its runtime TypeId and look the info up. // - // `thread_local!` is std-only, so under `no_std` there is no registry and - // the value-first entry points degrade (like exception catching does). + // Under `no_std`, the registry uses a non-threaded OnceInit-backed map. // Type-first reflection (typeof, MakeRecord, GetRecordElements) is // unaffected, as those carry the info in the `System.Type` value itself. - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] - thread_local! { - static RECORD_REGISTRY: RefCell>> = - RefCell::new(HashMap::new()); - } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] - static RECORD_REGISTRY: OnceLock>>> = OnceLock::new(); + #[cfg(not(feature = "threaded"))] + static RECORD_REGISTRY: OnceInit>>> = + OnceInit::new(); + + #[cfg(feature = "threaded")] + static RECORD_REGISTRY: OnceLock>>> = + OnceLock::new(); - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + #[cfg(not(feature = "threaded"))] fn registry_insert(tid: TypeId, info: LrcPtr) { - RECORD_REGISTRY.with(|r| { - r.borrow_mut().insert(tid, info); - }); + RECORD_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get_mut() + .insert(tid, info); } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + #[cfg(feature = "threaded")] fn registry_insert(tid: TypeId, info: LrcPtr) { RECORD_REGISTRY - .get_or_init(|| RwLock::new(HashMap::new())) + .get_or_init(|| RwLock::new(collections::HashMap::new())) .write() .unwrap() .insert(tid, info); } - #[cfg(feature = "no_std")] - fn registry_insert(_tid: TypeId, _info: LrcPtr) { - // no registry when no_std - } - - #[cfg(all(not(feature = "no_std"), not(feature = "threaded")))] + #[cfg(not(feature = "threaded"))] fn registry_get(tid: &TypeId) -> Option> { - RECORD_REGISTRY.with(|r| r.borrow().get(tid).cloned()) + RECORD_REGISTRY + .get_or_init(|| MutCell::new(collections::HashMap::new())) + .get() + .get(tid) + .cloned() } - #[cfg(all(not(feature = "no_std"), feature = "threaded"))] + #[cfg(feature = "threaded")] fn registry_get(tid: &TypeId) -> Option> { RECORD_REGISTRY - .get_or_init(|| RwLock::new(HashMap::new())) + .get_or_init(|| RwLock::new(collections::HashMap::new())) .read() .unwrap() .get(tid) .cloned() } - #[cfg(feature = "no_std")] - fn registry_get(_tid: &TypeId) -> Option> { - None // no registry when no_std - } - // Emitted by the generated record declaration's reflection method. Builds the field metadata, registers // the type by its concrete TypeId, and returns the boxed RecordTypeInfo // (which is what a `System.Type` value holds on the Rust target). diff --git a/tests/Rust/tests/src/RecordReflectionTests.fs b/tests/Rust/tests/src/RecordReflectionTests.fs index 3bd04ac322..483dc4513f 100644 --- a/tests/Rust/tests/src/RecordReflectionTests.fs +++ b/tests/Rust/tests/src/RecordReflectionTests.fs @@ -1,6 +1,5 @@ -// Value-based reflection uses a std-only type registry (it uses thread_local), so -// these tests do not apply to no_std builds. -[] +// Value-based reflection uses thread_local on std and OnceInit on no_std. +[] module Fable.Tests.RecordReflectionTests open Util.Testing From 67e8540a21183248826d1b65b3d8e3eddc8d5825 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 20:43:23 +0200 Subject: [PATCH 035/322] chore: release multiple projects (#4907) --- .github/workflows/easybuild-shipit.yml | 18 ++++---------- src/Fable.Cli/CHANGELOG.md | 32 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 31 +++++++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 25 ++++++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 14 ++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 25 ++++++++++++++++++- src/fable-standalone/package.json | 4 ++-- 10 files changed, 134 insertions(+), 25 deletions(-) diff --git a/.github/workflows/easybuild-shipit.yml b/.github/workflows/easybuild-shipit.yml index c3bf9922ea..1df8d71de9 100644 --- a/.github/workflows/easybuild-shipit.yml +++ b/.github/workflows/easybuild-shipit.yml @@ -28,9 +28,7 @@ jobs: # Adapt this value based on your needs. fetch-depth: 2000 - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet # Run your tests before generating the changelog to make sure everything is working fine before creating a pull request. - name: Prepare for release run: ./build.sh prepare-release @@ -46,18 +44,12 @@ jobs: if: "github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ')" steps: - uses: actions/checkout@v7.0.1 - # Configure how you want to publish your package, e.g. using the NuGet CLI, dotnet CLI, NPM, etc. - # It is recommanded to use Trusted Publishing to avoid having to manage API keys in your repository. + - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - global-json-file: global.json + uses: ./.github/actions/setup-dotnet - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '24' - registry-url: 'https://registry.npmjs.org' + - name: Setup Node.js environment + uses: ./.github/actions/setup-node - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index bba32a64f7..818c709b92 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.16.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(python)* Support quotation derived patterns (#4928) ([f6d939a1](https://github.com/fable-compiler/Fable/commit/f6d939a14c67010cb6cc02c7994d6f56c7156dab)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* Preserve project FSharp.Core reference (#4924) ([1289360a](https://github.com/fable-compiler/Fable/commit/1289360a11398387712df2769069b9c949659966)) +* Report Fable's own logs from the Fable.Compiler API (#4923) ([0775c604](https://github.com/fable-compiler/Fable/commit/0775c604d319d230796da0d47817428b1da8fece)) +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(python)* Compare nested options of different depth (#4940) ([5afa2948](https://github.com/fable-compiler/Fable/commit/5afa2948edd221b64aa14553509ba96028d064c5)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* Async perf: stop the async runtime sleeping, and add Parallel (#4920) ([88135b90](https://github.com/fable-compiler/Fable/commit/88135b90382f63bd40d58dfc75c4f915cc4bb868)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Wait for the other files instead of sleeping before the last one. (#4927) ([d329a823](https://github.com/fable-compiler/Fable/commit/d329a823298af9f934058f245d54155ccb555a69)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) +* *(rust)* Support reflection for no_std (#4941) ([bf694309](https://github.com/fable-compiler/Fable/commit/bf694309ae5dbc6bb546d9acc4d091f5e35fe330)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 5.15.0 - 2026-08-25 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index a2e5531e88..0c25319e01 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.15.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(python)* Support quotation derived patterns (#4928) ([f6d939a1](https://github.com/fable-compiler/Fable/commit/f6d939a14c67010cb6cc02c7994d6f56c7156dab)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* Preserve project FSharp.Core reference (#4924) ([1289360a](https://github.com/fable-compiler/Fable/commit/1289360a11398387712df2769069b9c949659966)) +* Report Fable's own logs from the Fable.Compiler API (#4923) ([0775c604](https://github.com/fable-compiler/Fable/commit/0775c604d319d230796da0d47817428b1da8fece)) +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(python)* Compare nested options of different depth (#4940) ([5afa2948](https://github.com/fable-compiler/Fable/commit/5afa2948edd221b64aa14553509ba96028d064c5)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* Async perf: stop the async runtime sleeping, and add Parallel (#4920) ([88135b90](https://github.com/fable-compiler/Fable/commit/88135b90382f63bd40d58dfc75c4f915cc4bb868)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) +* *(rust)* Support reflection for no_std (#4941) ([bf694309](https://github.com/fable-compiler/Fable/commit/bf694309ae5dbc6bb546d9acc4d091f5e35fe330)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 5.14.1 - 2026-08-25 ### 🐞 Bug Fixes diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 37ba379f99..c3e962d961 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.15.0" + let VERSION = "5.16.0" [] - let JS_LIBRARY_VERSION = "2.5.1" + let JS_LIBRARY_VERSION = "2.6.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index aba5fef45d..d7ea9aa913 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.2.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 3.1.0 - 2026-08-25 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 6501ad628b..7182debb89 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.1.0", + "version": "3.2.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.1.0" + "@fable-org/fable-standalone": "^3.2.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 8d7be77bf0..2077552206 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 619d7afddbaf25108e819abaaa9d8a6503c457d6 +last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 updaters: - package.json: file: package.json @@ -15,6 +15,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.6.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/619d7afddbaf25108e819abaaa9d8a6503c457d6..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 2.5.1 - 2026-07-24 ### 🐞 Bug Fixes diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index d37cea52f2..fc1de0835b 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.5.1", + "version": "2.6.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 094c23e52c..af35e136a4 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b517d52df145ee30d1d3bd7e20fded1a9003ac54 +last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -19,6 +19,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.2.0 - 2026-09-03 + +### 🚀 Features + +* *(all)* Quotations: DerivedPatterns, and captured locals as Value nodes (#4919) ([fae5093f](https://github.com/fable-compiler/Fable/commit/fae5093f2801195e865adf1acfaec63f38bbf973)) +* *(rust)* Observables, Events and IEnumerable (PR 6 of 7 Rust improvements) (#4789) ([20175eea](https://github.com/fable-compiler/Fable/commit/20175eea841adf14ed57c0068458fad591c8d5f4)) +* *(rust)* System.Uri (PR 7 of 7 Rust improvements) (#4790) ([8a551bc9](https://github.com/fable-compiler/Fable/commit/8a551bc9b2433035fc704dffc639e5f031ca7593)) + +### 🐞 Bug Fixes + +* *(all)* Emit instance calls for virtual methods inherited from Object (#4926) ([2fb535a7](https://github.com/fable-compiler/Fable/commit/2fb535a7cd35cef501c56684f205e2dffa3c36e6)) +* *(all)* Allow private inline functions to use private values (#4915) ([b0740ba9](https://github.com/fable-compiler/Fable/commit/b0740ba95859a80e7add07f21e2fc81a31c00853)) +* *(python)* Emit Python names for float32 specials (#4938) ([b4c45a93](https://github.com/fable-compiler/Fable/commit/b4c45a93385f249a8586789037b0af18005786e9)) +* *(rust)* Observables and Events cleanup (#4908) ([ca165045](https://github.com/fable-compiler/Fable/commit/ca165045703797f0558da4bc6e06cbea7af231de)) +* *(rust)* Support `string` on a tuple (#4916) ([510c8a43](https://github.com/fable-compiler/Fable/commit/510c8a43c159ffb9d7d7bdc4e2ee16b84b80b28e)) +* *(rust)* Decimal culture overloads, and mutables assigned in try/finally (#4918) ([58402471](https://github.com/fable-compiler/Fable/commit/584024712450d333193baddc239db977af05bd54)) +* *(rust)* Let a string be enumerated (#4921) ([395c152b](https://github.com/fable-compiler/Fable/commit/395c152bd4ef0c26af7a4ff48ed44c728aa73d25)) +* *(rust)* F# exceptions, non-exhaustive matches and obj type tests (#4917) ([28da1d76](https://github.com/fable-compiler/Fable/commit/28da1d764e63c39b96ca20e94d9a08786b3c9052)) +* *(rust)* Cleanup AsyncBuilder and TaskBuilder (#4936) ([3e6bc9bd](https://github.com/fable-compiler/Fable/commit/3e6bc9bd4d4337e901cc748f35df2a5933f0ba55)) +* *(rust)* Fix interface casting, move reflection into declarations (#4939) ([04299157](https://github.com/fable-compiler/Fable/commit/042991572969e1864a160b0a0145b1d59bc77236)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b517d52df145ee30d1d3bd7e20fded1a9003ac54..bf694309ae5dbc6bb546d9acc4d091f5e35fe330) + ## 3.1.0 - 2026-08-25 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 250004fe9f..f9603b5df7 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.1.0", - "fableVersion": "5.15.0", + "version": "3.2.0", + "fableVersion": "5.16.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 3e8f56f78345fbee8a4394c5452eb212264701f9 Mon Sep 17 00:00:00 2001 From: kerams Date: Mon, 7 Sep 2026 14:15:05 +0200 Subject: [PATCH 036/322] feat: add Temporal dates and times (opt-in) (#4759) Co-authored-by: Mangel Maxime --- src/Fable.AST/Plugins.fs | 1 + src/Fable.Build/Main.fs | 2 + src/Fable.Build/Test/JavaScript.fs | 47 +- src/Fable.Cli/Entry.fs | 8 +- src/Fable.Transforms/Babel/Fable2Babel.fs | 40 +- src/Fable.Transforms/Global/Compiler.fs | 4 +- src/Fable.Transforms/Replacements.fs | 448 ++++++++++--- src/fable-library-ts/DateOnlyTemporal.ts | 174 ++++++ src/fable-library-ts/DateTimeFormat.ts | 591 ++++++++++++++++++ .../DateTimeOffsetTemporal.ts | 355 +++++++++++ src/fable-library-ts/DateTimeTemporal.ts | 313 ++++++++++ src/fable-library-ts/Guid.ts | 6 +- src/fable-library-ts/String.ts | 22 +- src/fable-library-ts/TimeOnlyTemporal.ts | 181 ++++++ src/fable-library-ts/TimeSpanTemporal.ts | 280 +++++++++ src/fable-library-ts/ts/tsconfig.json | 2 +- src/fable-library-ts/tsconfig.json | 2 +- tests/Js/Main/DateTimeOffsetTests.fs | 36 ++ tests/Js/Main/DateTimeTests.fs | 54 ++ tests/Js/Main/StringTests.fs | 19 + tests/Js/Main/TimeOnlyTests.fs | 49 +- tests/Js/Main/TimeSpanTests.fs | 45 ++ tests/TypeScript/tsconfig.json | 3 + 23 files changed, 2556 insertions(+), 126 deletions(-) create mode 100644 src/fable-library-ts/DateOnlyTemporal.ts create mode 100644 src/fable-library-ts/DateTimeFormat.ts create mode 100644 src/fable-library-ts/DateTimeOffsetTemporal.ts create mode 100644 src/fable-library-ts/DateTimeTemporal.ts create mode 100644 src/fable-library-ts/TimeOnlyTemporal.ts create mode 100644 src/fable-library-ts/TimeSpanTemporal.ts diff --git a/src/Fable.AST/Plugins.fs b/src/Fable.AST/Plugins.fs index 930e0e8aa3..a39cee1354 100644 --- a/src/Fable.AST/Plugins.fs +++ b/src/Fable.AST/Plugins.fs @@ -40,6 +40,7 @@ type CompilerOptions = FileExtension: string TriggeredByDependency: bool NoReflection: bool + JsTemporal: bool } type PluginHelper = diff --git a/src/Fable.Build/Main.fs b/src/Fable.Build/Main.fs index 4473cda2cb..36f0b62085 100644 --- a/src/Fable.Build/Main.fs +++ b/src/Fable.Build/Main.fs @@ -59,6 +59,8 @@ Available commands: (can be run in watch mode) --adaptive-only Run only the tests for the adaptive version of Fable (can be run in watch mode) + --temporal-only Run only the main tests, compiled with the Temporal + date/time representation (can be run in watch mode) Options for Rust: diff --git a/src/Fable.Build/Test/JavaScript.fs b/src/Fable.Build/Test/JavaScript.fs index 9280ff78f7..4b978a26ad 100644 --- a/src/Fable.Build/Test/JavaScript.fs +++ b/src/Fable.Build/Test/JavaScript.fs @@ -86,11 +86,20 @@ let private testAdaptive (isWatch: bool) = else Command.Fable(fableArgs, workingDirectory = destinationDir) -let private handleMainTests (isWatch: bool) (noDotnet: bool) = +type private DateTimeRepresentation = + | JsDate + | Temporal + +let private runMainTests (representation: DateTimeRepresentation) (isWatch: bool) (noDotnet: bool) = let folderName = "Main" let sourceDir = Path.Resolve("tests", "Js", folderName) - let destinationDir = Path.Resolve("temp", "tests", "JavaScript", folderName) + let outputFolder = + match representation with + | JsDate -> "JavaScript" + | Temporal -> "JavaScriptTemporal" + + let destinationDir = Path.Resolve("temp", "tests", outputFolder, folderName) let testCommand = CmdLine.empty @@ -111,6 +120,7 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = |> CmdLine.appendPrefix "--lang" "javascript" |> CmdLine.appendPrefix "--exclude" "Fable.Core" |> CmdLine.appendRaw "--noCache" + |> CmdLine.appendIf (representation = Temporal) "--test:js-temporal" if isWatch then CmdLine.empty @@ -122,7 +132,6 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = ] if isWatch then - // In watch mode, we only test the Main tests to not pollute the logs too much Async.Parallel [ if not noDotnet then @@ -138,11 +147,21 @@ let private handleMainTests (isWatch: bool) (noDotnet: bool) = ] |> Async.RunSynchronously |> ignore + else + Command.Fable(fableArgs, workingDirectory = destinationDir) + +let private handleMainTests (isWatch: bool) (noDotnet: bool) = + if isWatch then + // In watch mode, we only test the Main tests to not pollute the logs too much + runMainTests JsDate isWatch noDotnet else Command.Run("dotnet", "run -c Release", workingDirectory = Path.Combine("tests", "Js", "Main")) // Test the Main tests against JavaScript - Command.Fable(fableArgs, workingDirectory = destinationDir) + runMainTests JsDate false noDotnet + + // Re-run them with the Temporal date/time representation enabled + runMainTests Temporal false noDotnet testReact false testAdaptive false @@ -157,17 +176,23 @@ let handle (args: string list) = let isReactOnly = args |> List.contains "--react-only" let isStandaloneOnly = args |> List.contains "--standalone-only" let isAdaptiveOnly = args |> List.contains "--adaptive-only" + let isTemporalOnly = args |> List.contains "--temporal-only" let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" - match (isReactOnly, isStandaloneOnly, isAdaptiveOnly) with - | (true, true, _) - | (true, _, true) - | (_, true, true) -> - failwith "Cannot use '--react-only', '--standalone-only' and '--adaptive-only' at the same time" + let exclusiveArgs = + [ + "--react-only", isReactOnly + "--standalone-only", isStandaloneOnly + "--adaptive-only", isAdaptiveOnly + "--temporal-only", isTemporalOnly + ] + |> List.filter snd + |> List.map (fun (name, _) -> $"'%s{name}'") - | _ -> () + if exclusiveArgs.Length > 1 then + failwith $"""Cannot use %s{String.Join(", ", exclusiveArgs)} at the same time""" BuildFableLibraryJavaScript().Run(forceFableLibrary) @@ -177,5 +202,7 @@ let handle (args: string list) = Standalone.handleStandaloneFast () else if isAdaptiveOnly then testAdaptive isWatch + else if isTemporalOnly then + runMainTests Temporal isWatch noDotnet else handleMainTests isWatch noDotnet diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index 74921cbfb8..a12b69be37 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -126,6 +126,7 @@ let knownCliArgs () = [ "--trimRootModule" ], [] [ "--fableLib" ], [] [ "--replace" ], [] + [ "--test:js-temporal" ], [] ] let printKnownCliArgs () = @@ -314,12 +315,16 @@ type Runner = | Some c when String.IsNullOrWhiteSpace c -> defaultConfiguration | Some configurationArg -> configurationArg + let jsTemporal = args.FlagEnabled "--test:js-temporal" + let define = args.Values "--define" |> List.append [ "FABLE_COMPILER" "FABLE_COMPILER_5" + if jsTemporal then + "FABLE_COMPILER_JAVASCRIPT_TEMPORAL" match language with | Php -> "FABLE_COMPILER_PHP" | Rust -> "FABLE_COMPILER_RUST" @@ -353,7 +358,8 @@ type Runner = debugMode = (configuration = "Debug"), optimizeFSharpAst = args.FlagEnabled "--optimize", noReflection = args.FlagEnabled "--noReflection", - verbosity = verbosity + verbosity = verbosity, + jsTemporal = jsTemporal ) let cliArgs = diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 19186f764d..c162811bd1 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -749,11 +749,31 @@ module Annotation = let makeBuiltinTypeAnnotation com ctx typ kind = match kind with | Replacements.Util.BclGuid -> StringTypeAnnotation - | Replacements.Util.BclTimeSpan -> NumberTypeAnnotation - | Replacements.Util.BclDateTime -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclDateTimeOffset -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclDateOnly -> makeAliasTypeAnnotation com ctx "Date" - | Replacements.Util.BclTimeOnly -> NumberTypeAnnotation + | Replacements.Util.BclTimeSpan -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "TimeSpanTemporal" "Duration" + else + NumberTypeAnnotation + | Replacements.Util.BclDateTime -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateTimeTemporal" "PlainDateTime" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclDateTimeOffset -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateTimeOffsetTemporal" "ZonedDateTime" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclDateOnly -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "DateOnlyTemporal" "PlainDate" + else + makeAliasTypeAnnotation com ctx "Date" + | Replacements.Util.BclTimeOnly -> + if com.Options.JsTemporal then + makeFableLibImportTypeAnnotation com ctx [] "TimeOnlyTemporal" "PlainTime" + else + NumberTypeAnnotation | Replacements.Util.BclTimer -> makeFableLibImportTypeAnnotation com ctx [] "Timer" "Timer" | Replacements.Util.BclHashSet key -> makeFableLibImportTypeAnnotation com ctx [ key ] "Util" "ISet" | Replacements.Util.BclDictionary(key, value) -> @@ -3518,11 +3538,15 @@ but thanks to the optimisation done below we get | Fable.Char -> Expression.stringLiteral "\u0000" | Fable.Number(kind, uom) -> com.TransformAsExpr(ctx, Fable.NumberConstant(Fable.NumberValue.GetZero kind, uom) |> makeValue None) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + libCall com ctx None "TimeOnlyTemporal" "minValue" [] [] + | Builtin BclTimeSpan when com.Options.JsTemporal -> libCall com ctx None "TimeSpanTemporal" "zero" [] [] | Builtin(BclTimeSpan | BclTimeOnly) -> Expression.numericLiteral 0 | Builtin BclGuid -> Expression.stringLiteral "00000000-0000-0000-0000-000000000000" - | Builtin BclDateTime -> libCall com ctx None "Date" "minValue" [] [] - | Builtin BclDateTimeOffset -> libCall com ctx None "DateOffset" "minValue" [] [] - | Builtin BclDateOnly -> libCall com ctx None "DateOnly" "minValue" [] [] + | Builtin BclDateTime -> libCall com ctx None (JS.Replacements.dateTimeModule com) "minValue" [] [] + | Builtin BclDateTimeOffset -> + libCall com ctx None (JS.Replacements.dateTimeOffsetModule com) "minValue" [] [] + | Builtin BclDateOnly -> libCall com ctx None (JS.Replacements.dateOnlyModule com) "minValue" [] [] | _ -> libCall com ctx None "Util" "defaultOf" [] [] let getEntityFieldsAsIdents (ent: Fable.Entity) = diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index c3e962d961..12be0f42a4 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -18,7 +18,8 @@ type CompilerOptionsHelper = ?verbosity, ?fileExtension, ?clampByteArrays, - ?noReflection + ?noReflection, + ?jsTemporal ) = { @@ -32,6 +33,7 @@ type CompilerOptionsHelper = ClampByteArrays = defaultArg clampByteArrays false NoReflection = defaultArg noReflection false TriggeredByDependency = false + JsTemporal = defaultArg jsTemporal false } [] diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index c553e134a5..eec49d7335 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -96,6 +96,54 @@ let coreModFor = | BclDictionary _ -> "MutableMap" | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise +let dateOnlyModule (com: Compiler) = + if com.Options.JsTemporal then + "DateOnlyTemporal" + else + "DateOnly" + +let timeOnlyModule (com: Compiler) = + if com.Options.JsTemporal then + "TimeOnlyTemporal" + else + "TimeOnly" + +let timeSpanModule (com: Compiler) = + if com.Options.JsTemporal then + "TimeSpanTemporal" + else + "TimeSpan" + +let dateTimeModule (com: Compiler) = + if com.Options.JsTemporal then + "DateTimeTemporal" + else + "Date" + +let dateTimeOffsetModule (com: Compiler) = + if com.Options.JsTemporal then + "DateTimeOffsetTemporal" + else + "DateOffset" + +let private temporalSubtractionName opName (argTypes: Type list) = + if opName <> Operators.subtraction then + opName + else + match List.tryLast argTypes with + // DateTime - DateTime and DateTimeOffset - DateTimeOffset yield a TimeSpan + | Some(Builtin(BclDateTime | BclDateTimeOffset)) -> "subtractDate" + | _ -> "subtractTimeSpan" + +let private temporalDivisionName opName (argTypes: Type list) = + if opName <> Operators.division then + opName + else + match List.tryLast argTypes with + // TimeSpan / TimeSpan yields a ratio, TimeSpan / float another TimeSpan + | Some(Builtin BclTimeSpan) -> "divideByTimeSpan" + | _ -> "divide" + let makeDecimal com r t (x: decimal) = let str = x.ToString(System.Globalization.CultureInfo.InvariantCulture) Helper.LibCall(com, "Decimal", "default", t, [ makeStrConst str ], isConstructor = true, ?loc = r) @@ -358,7 +406,15 @@ let toString com (ctx: Context) r (args: Expr list) = | String -> head | Char -> TypeCast(head, String) | Builtin BclGuid when tail.IsEmpty -> head - | Builtin(BclGuid | BclTimeSpan | BclTimeOnly | BclDateOnly | BclDateTime | BclDateTimeOffset as bt) -> + | Builtin BclDateOnly -> Helper.LibCall(com, dateOnlyModule com, "toString", String, args) + | Builtin BclTimeOnly -> Helper.LibCall(com, timeOnlyModule com, "toString", String, args) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, timeSpanModule com, "toString", String, args) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, dateTimeModule com, "toString", String, args) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, dateTimeOffsetModule com, "toString", String, args) + | Builtin(BclGuid | BclTimeSpan | BclDateTime | BclDateTimeOffset as bt) -> Helper.LibCall(com, coreModFor bt, "toString", String, args) | Number(Int16, _) -> Helper.LibCall(com, "Util", "int16ToString", String, args) | Number(Int32, _) -> Helper.LibCall(com, "Util", "int32ToString", String, args) @@ -468,6 +524,18 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = op else wrapLong com ctx r t op + | Builtin BclDateTime :: _ when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", temporalSubtractionName opName argTypes, t, args, argTypes, ?loc = r) + | Builtin BclDateTimeOffset :: _ when com.Options.JsTemporal -> + Helper.LibCall( + com, + "DateTimeOffsetTemporal", + temporalSubtractionName opName argTypes, + t, + args, + argTypes, + ?loc = r + ) | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly as bt) :: _ -> Helper.LibCall(com, coreModFor bt, opName, t, args, argTypes, ?loc = r) | Builtin(FSharpSet _) :: _ -> @@ -477,13 +545,17 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = // | Builtin (FSharpMap _)::_ -> // let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpMap" true opName overloadSuffix.Value // Helper.LibCall(com, "Map", mangledName, t, args, argTypes, ?loc=r) + | Builtin BclTimeSpan :: _ when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", temporalDivisionName opName argTypes, t, args, argTypes, ?loc = r) | Builtin BclTimeSpan :: _ -> nativeOp opName argTypes args | CustomOp com ctx r t opName args e -> e | _ -> nativeOp opName argTypes args -let isCompatibleWithNativeComparison = - function - | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) +let isCompatibleWithNativeComparison (com: Compiler) typ = + match typ with + // Temporal.PlainTime and Temporal.Duration cannot be compared natively + | Builtin(BclTimeOnly | BclTimeSpan) -> not com.Options.JsTemporal + | Builtin BclGuid | Boolean | Char | String @@ -497,55 +569,74 @@ let isCompatibleWithNativeComparison = // * `.GetHashCode` called directly defaults to identity hash (for reference types except string) if not implemented. // * `LanguagePrimitive.PhysicalHash` creates an identity hash no matter whether GetHashCode is implemented or not. -let identityHash com r (arg: Expr) = - let methodName = - match arg.Type with - // These are the same for identity/structural hashing - | Char - | String - | Builtin BclGuid -> "stringHash" - | Number(Decimal, _) -> "safeHash" - | Number(BigIntegers _, _) -> "bigintHash" - | Number(Numbers _, _) -> "numberHash" - | Builtin BclTimeSpan - | Builtin BclTimeOnly -> "numberHash" - | List _ -> "safeHash" - | Tuple _ -> "arrayHash" // F# tuples must use structural hashing - // These are only used for structural hashing - // | Array _ -> "arrayHash" - // | Builtin (BclDateTime|BclDateTimeOffset) -> "dateHash" - | DeclaredType _ -> "safeHash" - | _ -> "identityHash" - - Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) +let identityHash (com: ICompiler) r (arg: Expr) = + match arg.Type with + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | _ -> + let methodName = + match arg.Type with + // These are the same for identity/structural hashing + | Char + | String + | Builtin BclGuid -> "stringHash" + | Number(Decimal, _) -> "safeHash" + | Number(BigIntegers _, _) -> "bigintHash" + | Number(Numbers _, _) -> "numberHash" + | Builtin BclTimeSpan + | Builtin BclTimeOnly -> "numberHash" + | List _ -> "safeHash" + | Tuple _ -> "arrayHash" // F# tuples must use structural hashing + // These are only used for structural hashing + // | Array _ -> "arrayHash" + // | Builtin (BclDateTime|BclDateTimeOffset) -> "dateHash" + | DeclaredType _ -> "safeHash" + | _ -> "identityHash" + + Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) let structuralHash (com: ICompiler) r (arg: Expr) = - let methodName = - match arg.Type with - | Char - | String - | Builtin BclGuid -> "stringHash" - | Number(Decimal, _) -> "fastStructuralHash" - | Number(BigIntegers _, _) -> "bigintHash" - | Number(Numbers _, _) -> "numberHash" - | Builtin BclTimeSpan - | Builtin BclTimeOnly -> "numberHash" - | List _ -> "safeHash" - // TODO: Get hash functions of the generic arguments - // for better performance when using tuples as map keys - | Tuple _ - | Array _ -> "arrayHash" - | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> "dateHash" - | DeclaredType(ent, _) -> - let ent = com.GetEntity(ent) + match arg.Type with + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "hash", Int32.Number, [ arg ], ?loc = r) + | _ -> - if not ent.IsInterface then - "safeHash" - else - "structuralHash" - | _ -> "structuralHash" + let methodName = + match arg.Type with + | Char + | String + | Builtin BclGuid -> "stringHash" + | Number(Decimal, _) -> "fastStructuralHash" + | Number(BigIntegers _, _) -> "bigintHash" + | Number(Numbers _, _) -> "numberHash" + | Builtin BclTimeSpan + | Builtin BclTimeOnly -> "numberHash" + | List _ -> "safeHash" + // TODO: Get hash functions of the generic arguments + // for better performance when using tuples as map keys + | Tuple _ + | Array _ -> "arrayHash" + | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> "dateHash" + | DeclaredType(ent, _) -> + let ent = com.GetEntity(ent) - Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) + if not ent.IsInterface then + "safeHash" + else + "structuralHash" + | _ -> "structuralHash" + + Helper.LibCall(com, "Util", methodName, Int32.Number, [ arg ], ?loc = r) let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = let is equal expr = @@ -561,6 +652,12 @@ let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = | Number(BigIntegers _, _) -> Helper.LibCall(com, "BigInt", "equals", Boolean, [ left; right ], ?loc = r) |> is equal + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) | Boolean | Char @@ -576,6 +673,15 @@ let rec equals (com: ICompiler) ctx r equal (left: Expr) (right: Expr) = // Use BinaryEquals for MetaType to have a change of optimization in FableTransforms.operationReduction // We will call Reflection.equals in the Fable2Babel step //| MetaType -> Helper.LibCall(com, "Reflection", "equals", Boolean, [left; right], ?loc=r) |> is equal + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "equals", Boolean, [ left; right ], ?loc = r) + |> is equal | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> Helper.LibCall(com, "Date", "equals", Boolean, [ left; right ], ?loc = r) |> is equal @@ -611,11 +717,21 @@ and compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = match left.Type with | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "compare", t, [ left; right ], ?loc = r) | Number(BigIntegers _, _) -> Helper.LibCall(com, "BigInt", "compare", t, [ left; right ], ?loc = r) + | Builtin BclTimeOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeOnlyTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclTimeSpan when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "compare", t, [ left; right ], ?loc = r) | Builtin(BclGuid | BclTimeSpan | BclTimeOnly) | Boolean | Char | String | Number _ -> Helper.LibCall(com, "Util", "comparePrimitives", t, [ left; right ], ?loc = r) + | Builtin BclDateOnly when com.Options.JsTemporal -> + Helper.LibCall(com, "DateOnlyTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclDateTime when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeTemporal", "compare", t, [ left; right ], ?loc = r) + | Builtin BclDateTimeOffset when com.Options.JsTemporal -> + Helper.LibCall(com, "DateTimeOffsetTemporal", "compare", t, [ left; right ], ?loc = r) | Builtin(BclDateTime | BclDateTimeOffset | BclDateOnly) -> Helper.LibCall(com, "Date", "compare", t, [ left; right ], ?loc = r) | DeclaredType _ -> Helper.LibCall(com, "Util", "compare", t, [ left; right ], ?loc = r) @@ -629,7 +745,7 @@ and compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = /// Boolean comparison operators like <, >, <=, >= and booleanCompare (com: ICompiler) ctx r (left: Expr) (right: Expr) op = - if isCompatibleWithNativeComparison left.Type then + if isCompatibleWithNativeComparison com left.Type then makeEqOp r left right op else let comparison = compare com ctx r left right @@ -681,7 +797,7 @@ let makeDictionaryWithComparer com r t sourceSeq comparer = let makeDictionary (com: ICompiler) ctx r t sourceSeq = match t with - | DeclaredType(_, [ key; _ ]) when not (isCompatibleWithNativeComparison key) -> + | DeclaredType(_, [ key; _ ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key makeEqualityComparer com ctx key |> makeDictionaryWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Map", t, [ sourceSeq ], isConstructor = true, ?loc = r) @@ -691,13 +807,23 @@ let makeHashSetWithComparer com r t sourceSeq comparer = let makeHashSet (com: ICompiler) ctx r t sourceSeq = match t with - | DeclaredType(_, [ key ]) when not (isCompatibleWithNativeComparison key) -> + | DeclaredType(_, [ key ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key makeEqualityComparer com ctx key |> makeHashSetWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Set", t, [ sourceSeq ], isConstructor = true, ?loc = r) let tryEntityIdent (com: Compiler) entFullName = match entFullName with + | BuiltinDefinition BclDateOnly when com.Options.JsTemporal -> + makeImportLib com Any "PlainDate" "DateOnlyTemporal" |> Some + | BuiltinDefinition BclTimeOnly when com.Options.JsTemporal -> + makeImportLib com Any "PlainTime" "TimeOnlyTemporal" |> Some + | BuiltinDefinition BclTimeSpan when com.Options.JsTemporal -> + makeImportLib com Any "Duration" "TimeSpanTemporal" |> Some + | BuiltinDefinition BclDateTime when com.Options.JsTemporal -> + makeImportLib com Any "PlainDateTime" "DateTimeTemporal" |> Some + | BuiltinDefinition BclDateTimeOffset when com.Options.JsTemporal -> + makeImportLib com Any "ZonedDateTime" "DateTimeOffsetTemporal" |> Some | BuiltinDefinition BclDateOnly | BuiltinDefinition BclDateTime | BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "Date" |> Some @@ -772,10 +898,12 @@ and private getZero (com: ICompiler) (ctx: Context) (t: Type) = | Char -> makeCharConst '\000' | String -> makeStrConst "" // TODO: Use null for string? | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None + | Builtin BclTimeOnly when com.Options.JsTemporal -> Helper.LibCall(com, "TimeOnlyTemporal", "minValue", t, []) + | Builtin BclTimeSpan when com.Options.JsTemporal -> Helper.LibCall(com, "TimeSpanTemporal", "zero", t, []) | Builtin(BclTimeSpan | BclTimeOnly) -> TypeCast(makeIntConst 0, t) - | Builtin BclDateTime as t -> Helper.LibCall(com, "Date", "minValue", t, []) - | Builtin BclDateTimeOffset as t -> Helper.LibCall(com, "DateOffset", "minValue", t, []) - | Builtin BclDateOnly as t -> Helper.LibCall(com, "DateOnly", "minValue", t, []) + | Builtin BclDateTime as t -> Helper.LibCall(com, dateTimeModule com, "minValue", t, []) + | Builtin BclDateTimeOffset as t -> Helper.LibCall(com, dateTimeOffsetModule com, "minValue", t, []) + | Builtin BclDateOnly as t -> Helper.LibCall(com, dateOnlyModule com, "minValue", t, []) | Builtin(FSharpSet genArg) as t -> makeSet com ctx None t "Empty" [] genArg | Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ] | ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e @@ -3232,7 +3360,7 @@ let ignoreFormatProvider meth args = | _ -> args let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName = "Date" + let moduleName = dateTimeModule com match i.CompiledName with | ".ctor" -> @@ -3242,8 +3370,12 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ExprType(DeclaredType(ent, [])) :: _ when ent.FullName = Types.dateOnly -> - Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + if com.Options.JsTemporal then + Helper.LibCall(com, "DateOnlyTemporal", "toDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + else + Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> let last = List.last args @@ -3253,7 +3385,7 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let argTypes = (List.take 6 i.SignatureArgTypes) @ [ Int32.Number; last.Type ] - Helper.LibCall(com, "Date", "create", t, args, argTypes, ?loc = r) |> Some + Helper.LibCall(com, moduleName, "create", t, args, argTypes, ?loc = r) |> Some // JavaScript Date doesn't support microseconds precision | 8, Number(Int32, NumberInfo.Empty) -> @@ -3269,14 +3401,35 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> - Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "Subtract" when com.Options.JsTemporal -> + // Overloaded on the argument: DateTime -> TimeSpan, TimeSpan -> DateTime + let meth = + match i.SignatureArgTypes with + | [ Builtin BclDateTime ] -> "subtractDate" + | _ -> "subtractTimeSpan" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "get_Kind" -> - Helper.LibCall(com, "Date", "getKind", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getKind", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some | "get_Ticks" -> - Helper.LibCall(com, "Date", "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_Hour" + | "get_Minute" + | "get_Second" + | "get_Millisecond" + | "get_DayOfYear" when com.Options.JsTemporal -> + // These map one-to-one onto the equivalent Temporal.PlainDateTime properties + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst @@ -3286,7 +3439,7 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let moduleName = "DateOffset" + let moduleName = dateTimeOffsetModule com match i.CompiledName with | ".ctor" -> @@ -3299,8 +3452,12 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E Helper.LibCall(com, moduleName, "fromDate", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ExprType(DeclaredType(ent, [])) :: _ when ent.FullName = Types.dateOnly -> - Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + if com.Options.JsTemporal then + Helper.LibCall(com, "DateOnlyTemporal", "toDateTimeOffset", t, args, i.SignatureArgTypes, ?loc = r) + |> Some + else + Helper.LibCall(com, moduleName, "fromDateTime", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> match args.Length with | 7 @@ -3316,14 +3473,36 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E Helper.LibCall(com, moduleName, "fromDate", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> - Helper.LibCall(com, "Date", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + // The Temporal module formats itself; the JS-Date one delegates to Date.toString (dispatches on offset) + let toStringModule = + if com.Options.JsTemporal then + moduleName + else + "Date" + + Helper.LibCall(com, toStringModule, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "get_Offset" -> Helper.LibCall(com, moduleName, "offset", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "Subtract" when com.Options.JsTemporal -> + // Overloaded on the argument: DateTimeOffset -> TimeSpan, TimeSpan -> DateTimeOffset + let meth = + match i.SignatureArgTypes with + | [ Builtin BclDateTimeOffset ] -> "subtractDate" + | _ -> "subtractTimeSpan" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "get_UtcDateTime" when com.Options.JsTemporal -> + Helper.LibCall(com, moduleName, "utcDateTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + |> Some | "get_UtcDateTime" -> Helper.LibCall(com, "DateOffset", "toUniversalTime", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_DateTime" when com.Options.JsTemporal -> + Helper.LibCall(com, moduleName, "dateTimeProp", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + |> Some | "get_DateTime" -> let kind = DateTimeKind.Unspecified |> int |> makeIntConst @@ -3338,11 +3517,30 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E ) |> Some | "get_Ticks" -> - Helper.LibCall(com, "Date", "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + // Non-temporal DateTimeOffset reuses Date.getTicks; the Temporal module has its own + let ticksModule = + if com.Options.JsTemporal then + moduleName + else + "Date" + + Helper.LibCall(com, ticksModule, "getTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some | "get_UtcTicks" -> - Helper.LibCall(com, "DateOffset", "getUtcTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) + Helper.LibCall(com, moduleName, "getUtcTicks", t, [ thisArg.Value ], [ thisArg.Value.Type ], ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_Hour" + | "get_Minute" + | "get_Second" + | "get_Millisecond" + | "get_DayOfYear" when com.Options.JsTemporal -> + // These map one-to-one onto the equivalent Temporal.ZonedDateTime properties + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst @@ -3352,6 +3550,8 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = dateOnlyModule com + match i.CompiledName with | ".ctor" when args.Length = 4 -> "DateOnly constructor with the calendar parameter is not supported." @@ -3359,7 +3559,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | ".ctor" -> - Helper.LibCall(com, "DateOnly", "create", t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?loc = r) |> Some | "ToString" -> match args with @@ -3370,7 +3570,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | [ StringConst("d" | "o" | "O"); _ ] -> - Helper.LibCall(com, "DateOnly", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | [ StringConst _; _ ] -> "DateOnly.ToString doesn't support custom format. It only handles \"d\", \"o\", \"O\" format, with CultureInfo.InvariantCulture." @@ -3380,7 +3580,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall( com, - "DateOnly", + moduleName, "toString", t, makeStrConst "d" :: args, @@ -3392,24 +3592,33 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | _ -> None | "AddDays" | "AddMonths" - | "AddYears" -> + | "AddYears" when not com.Options.JsTemporal -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst Helper.LibCall(com, "Date", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some + | "get_Year" + | "get_Month" + | "get_Day" + | "get_DayOfYear" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | meth -> let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = timeSpanModule com + let timeSpanLibCall meth args = let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - Helper.LibCall(com, "TimeSpan", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let isNotDefaultInt64ZeroValue (expr: Expr) = @@ -3439,9 +3648,34 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | [ _ticks ] -> "fromTicks" | _ -> "create" - Helper.LibCall(com, "TimeSpan", meth, t, args, i.SignatureArgTypes, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some - | "get_TotalMilliseconds" -> TypeCast(thisArg.Value, t) |> Some + | "get_TotalMilliseconds" when not com.Options.JsTemporal -> TypeCast(thisArg.Value, t) |> Some + | "op_Multiply" -> + // .NET overloads the operator on both sides: TimeSpan * float and float * TimeSpan + let args, argTypes = + match i.SignatureArgTypes with + | Builtin BclTimeSpan :: _ -> args, i.SignatureArgTypes + | _ -> List.rev args, List.rev i.SignatureArgTypes + + Helper.LibCall(com, moduleName, "multiply", t, args, argTypes, ?loc = r) |> Some + | "Divide" when com.Options.JsTemporal -> + // Overloaded on the argument: TimeSpan -> float ratio, float -> TimeSpan + let meth = + match i.SignatureArgTypes with + | [ Builtin BclTimeSpan ] -> "divideByTimeSpan" + | _ -> "divide" + + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + |> Some + | "get_Days" + | "get_Hours" + | "get_Minutes" + | "get_Seconds" + | "get_Milliseconds" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some | "ToString" when (args.Length = 1) -> "TimeSpan.ToString with one argument is not supported, because it depends on local culture, please add CultureInfo.InvariantCulture" |> addError com ctx.InlinePath r @@ -3452,7 +3686,7 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | StringConst "c" | StringConst "g" | StringConst "G" -> - Helper.LibCall(com, "TimeSpan", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | _ -> "TimeSpan.ToString don't support custom format. It only handles \"c\", \"g\" and \"G\" format, with CultureInfo.InvariantCulture." @@ -3468,26 +3702,37 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg | meth -> timeSpanLibCall meth args let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let moduleName = timeOnlyModule com + match i.CompiledName with | ".ctor" -> match args with | [ ExprType(Number(Int64, _)) ] -> - Helper.LibCall(com, "TimeOnly", "fromTicks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "fromTicks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | _ -> - Helper.LibCall(com, "TimeOnly", "create", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "create", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some - | "get_MinValue" -> makeIntConst 0 |> Some - | "ToTimeSpan" -> + | "get_MinValue" when not com.Options.JsTemporal -> makeIntConst 0 |> Some + | "ToTimeSpan" when not com.Options.JsTemporal -> // The representation is identical thisArg | "get_Hour" | "get_Minute" | "get_Second" + | "get_Millisecond" + | "get_Microsecond" + | "get_Nanosecond" when com.Options.JsTemporal -> + let fieldName = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst + + getFieldWith r t thisArg.Value fieldName |> Some + | "get_Hour" + | "get_Minute" + | "get_Second" | "get_Millisecond" -> // Translate TimeOnly properties with a name in singular to the equivalent properties on TimeSpan timeSpans com ctx r t { i with CompiledName = i.CompiledName + "s" } thisArg args - | "get_Ticks" -> + | "get_Ticks" when not com.Options.JsTemporal -> Helper.LibCall(com, "TimeSpan", "ticks", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | "ToString" -> @@ -3499,7 +3744,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op None | [ StringConst("r" | "R" | "o" | "O" | "t" | "T"); _ ] -> - Helper.LibCall(com, "TimeOnly", "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, "toString", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | [ StringConst _; _ ] -> "TimeOnly.ToString doesn't support custom format. It only handles \"r\", \"R\", \"o\", \"O\", \"t\", \"T\" format, with CultureInfo.InvariantCulture." @@ -3509,7 +3754,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall( com, - "TimeOnly", + moduleName, "toString", t, makeStrConst "t" :: args, @@ -3523,7 +3768,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let args = ignoreFormatProvider meth args let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst - Helper.LibCall(com, "TimeOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -4060,6 +4305,24 @@ let guids match i.CompiledName with | "NewGuid" -> Helper.LibCall(com, "Guid", "newGuid", t, []) |> Some + | "CreateVersion7" when com.Options.JsTemporal -> + // The Temporal DateTimeOffset is not a JS Date, so pass its Unix milliseconds instead + let args = + match args with + | [ dto ] -> + [ + Helper.LibCall( + com, + "DateTimeOffsetTemporal", + "toUnixTimeMilliseconds", + Int64.Number, + [ dto ], + [ dto.Type ] + ) + ] + | _ -> args + + Helper.LibCall(com, "Guid", "createVersion7", t, args) |> Some | "CreateVersion7" -> Helper.LibCall(com, "Guid", "createVersion7", t, args, i.SignatureArgTypes) |> Some @@ -4394,9 +4657,20 @@ let tryField com returnTyp ownerTyp fieldName = | Number(Decimal, _), _ -> Helper.LibValue(com, "Decimal", "get_" + fieldName, returnTyp) |> Some | String, "Empty" -> makeStrConst "" |> Some | Builtin BclGuid, "Empty" -> emptyGuid () |> Some + | Builtin BclTimeSpan, "Zero" when com.Options.JsTemporal -> + Helper.LibCall(com, "TimeSpanTemporal", "zero", returnTyp, []) |> Some | Builtin BclTimeSpan, "Zero" -> makeIntConst 0 |> Some - | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclDateOnly as t), ("MaxValue" | "MinValue") -> - Helper.LibCall(com, coreModFor t, Naming.lowerFirst fieldName, returnTyp, []) + | Builtin BclDateOnly, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateOnlyModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclTimeOnly, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, timeOnlyModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclDateTime, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateTimeModule com, Naming.lowerFirst fieldName, returnTyp, []) + |> Some + | Builtin BclDateTimeOffset, ("MaxValue" | "MinValue") -> + Helper.LibCall(com, dateTimeOffsetModule com, Naming.lowerFirst fieldName, returnTyp, []) |> Some | DeclaredType(ent, genArgs), fieldName -> match ent.FullName with diff --git a/src/fable-library-ts/DateOnlyTemporal.ts b/src/fable-library-ts/DateOnlyTemporal.ts new file mode 100644 index 0000000000..30a476bb99 --- /dev/null +++ b/src/fable-library-ts/DateOnlyTemporal.ts @@ -0,0 +1,174 @@ +import { FSharpRef } from "./Types.ts"; +import { TimeOnly } from "./TimeOnlyTemporal.ts"; +import { TimeSpan } from "./TimeSpanTemporal.ts"; +import { DateTime, dateTime as DateTime_stamp } from "./DateTimeTemporal.ts"; +import { DateTimeOffset, fromDate as DateTimeOffset_fromDate } from "./DateTimeOffsetTemporal.ts"; +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; + +export type DateOnly = Temporal.PlainDate; + +export const PlainDate = Temporal.PlainDate; +export type PlainDate = Temporal.PlainDate; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods (with a special case for JS +// Date, which Temporal.PlainDate cannot benefit from). Attach them so DateOnly +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.PlainDate.prototype as any; +proto.Equals = function (this: DateOnly, other: DateOnly): boolean { return this.equals(other); }; +proto.CompareTo = function (this: DateOnly, other: DateOnly): number { return Temporal.PlainDate.compare(this, other); }; +proto.GetHashCode = function (this: DateOnly): number { return dayNumber(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateOnly, format?: string): string { return toString(this, format); }; + +export function create(year: number, month: number, day: number): DateOnly { + return new Temporal.PlainDate(year, month, day); +} + +export function maxValue(): DateOnly { + return new Temporal.PlainDate(9999, 12, 31); +} + +export function minValue(): DateOnly { + return new Temporal.PlainDate(1, 1, 1); +} + +export function dayNumber(d: DateOnly): number { + return minValue().until(d).days; +} + +export function fromDayNumber(dayNumber: number): DateOnly { + return minValue().add({ days: dayNumber }); +} + +export function fromDateTime(d: Temporal.PlainDateTime): DateOnly { + // Under the Temporal representation a DateTime is a PlainDateTime (kind-agnostic wall-clock) + return new Temporal.PlainDate(d.year, d.month, d.day); +} + +export function dayOfWeek(d: DateOnly): number { + // Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6 + return d.dayOfWeek % 7; +} + +export function toDateTime(d: DateOnly, time: TimeOnly, kind: DateTimeKind = DateTimeKind.Unspecified): DateTime { + return DateTime_stamp(d.toPlainDateTime(time), kind); +} + +export function toDateTimeOffset(d: DateOnly, time: TimeOnly, offset: TimeSpan): DateTimeOffset { + return DateTimeOffset_fromDate(DateTime_stamp(d.toPlainDateTime(time), DateTimeKind.Unspecified), offset); +} + +export function addDays(d: DateOnly, v: number): DateOnly { + return d.add({ days: v }); +} + +export function addMonths(d: DateOnly, v: number): DateOnly { + return d.add({ months: v }); +} + +export function addYears(d: DateOnly, v: number): DateOnly { + return d.add({ years: v }); +} + +export function equals(x: DateOnly, y: DateOnly): boolean { + return x.equals(y); +} + +export function compare(x: DateOnly, y: DateOnly): number { + return Temporal.PlainDate.compare(x, y); +} + +export function hash(d: DateOnly): number { + return dayNumber(d); +} + +export function toString(d: DateOnly, format = "d", _provider?: any): string { + switch (format) { + case "d": + return `${padWithZeros(d.month, 2)}/${padWithZeros(d.day, 2)}/${padWithZeros(d.year, 4)}`; + case "o": + case "O": + // PlainDate.toString() is the ISO yyyy-MM-dd round-trip format + return d.toString(); + default: + throw new Exception("Custom formats are not supported"); + } +} + +export function parse(str: string): DateOnly { + function fail(): DateOnly { + throw new Exception(`String '${str}' was not recognized as a valid DateOnly.`); + } + + // Allowed separators: . , / - + // TODO whitespace alone as the separator + // + // Whitespace around separators + // + // Allowed format types: + // yyyy/mm/dd + // mm/dd/yyyy + // mm/dd + // mm/yyyy + // yyyy/mm + const r = /^\s*(\d{1,4})(?:\s*[.,-\/]\s*(\d{1,2}))?\s*[.,-\/]\s*(\d{1,4})\s*$/.exec(str); + if (r != null) { + let y = 0; + let m = 0; + let d = 1; + + if (r[2] == null) { + if (r[1].length < 3) { + if (r[3].length < 3) { + // 12/30 = December 30, {CurrentYear} + y = Temporal.Now.plainDateISO().year; + m = +r[1]; + d = +r[3]; + } else { + // 12/2000 = December 1, 2000 + m = +r[1]; + y = +r[3]; + } + } else { + if (r[3].length > 2) + fail(); + + // 2000/12 = December 1, 2000 + y = +r[1]; + m = +r[3]; + } + } else { + // 2000/1/30 or 1/30/2000 + const yearFirst = r[1].length > 2; + const yTmp = r[yearFirst ? 1 : 3]; + y = +yTmp; + + // year 0-29 is 2000-2029, 30-99 is 1930-1999 + if (yTmp.length < 3) + y += y >= 30 ? 1900 : 2000; + + m = +r[yearFirst ? 2 : 1]; + d = +r[yearFirst ? 3 : 2]; + } + + if (y > 0) { + try { + return new Temporal.PlainDate(y, m, d); + } catch { + return fail(); + } + } + } + + return fail(); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/DateTimeFormat.ts b/src/fable-library-ts/DateTimeFormat.ts new file mode 100644 index 0000000000..87548cfa6a --- /dev/null +++ b/src/fable-library-ts/DateTimeFormat.ts @@ -0,0 +1,591 @@ +/** + * Field-based .NET date/time formatting and lenient parsing, shared by the + * Temporal DateTime / DateTimeOffset representations. + * + * The formatters operate on a plain `DateInfo` (wall-clock fields), which the + * Temporal types expose directly (`d.year`, `d.hour`, `d.dayOfWeek % 7`, ...), + * so no JS-Date intermediate is needed there. + * + * Parsing is the exception. .NET accepts far more than ISO 8601 ("12/30/2009", + * "June 15, 2009 1:45 PM"), and the JS `Date` built-in is the only lenient date + * parser the platform offers — Temporal's `from` is strict ISO and rejects all of + * it. So `parseRaw` uses `Date` as a parsing engine and returns the instant it + * resolved, which callers convert into the Temporal value they need. That instant + * is only millisecond-precise, hence `subMillisecondTicks`: the finer digits are + * read straight off the input, because `Date` has already discarded them. + * + * This is a port of the equivalent logic in Date.ts / DateOffset.ts, kept + * separate so the Temporal modules do not depend on the JS-Date representation. + */ + +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; + +// Wall-clock fields. dayOfWeek is .NET-style: 0 = Sunday ... 6 = Saturday. +// +// `tick` is the sub-second component in .NET ticks (100ns units), 0..9_999_999 — +// not milliseconds. .NET's fractional-second format specifiers ("f"/"F", and the +// round-trip "O") go all the way down to a single tick, so carrying only +// milliseconds here would cap the output at 3 of the 7 digits .NET prints. +export interface DateInfo { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + tick: number; + dayOfWeek: number; +} + +// The 7 fractional-second digits .NET works with, most significant first. +function fractionDigits(info: DateInfo): string { + return padWithZeros(info.tick, 7); +} + +type OffsetInMinutes = number; +export type Offset = "Z" | OffsetInMinutes | null; + +const shortDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const longDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +const shortMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const longMonths = + ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + +function parseRepeatToken(format: string, pos: number, patternChar: string) { + let tokenLength = 0; + let internalPos = pos; + while (internalPos < format.length && format[internalPos] === patternChar) { + internalPos++; + tokenLength++; + } + + return tokenLength; +} + +function parseNextChar(format: string, pos: number) { + if (pos >= format.length - 1) { + return -1; + } + + return format.charCodeAt(pos + 1); +} + +function parseQuotedString(format: string, pos: number): [string, number] { + let beginPos = pos; + // Get the character used to quote the string + const quoteChar = format[pos]; + + let result = ""; + let foundQuote = false; + + while (pos < format.length) { + pos++; + const currentChar = format[pos]; + if (currentChar === quoteChar) { + foundQuote = true; + break; + } else if (currentChar === "\\") { + if (pos < format.length) { + pos++; + result += format[pos]; + } else { + // This means that '\' is the last character in the string. + throw new Exception("Invalid string format"); + } + } else { + result += currentChar; + } + } + + if (!foundQuote) { + // We could not find the matching quote + throw new Exception(`Invalid string format could not find matching quote for ${quoteChar}`); + } + + return [result, pos - beginPos + 1]; +} + +// Custom format string. `kind` decides the K/z tokens; `hostOffsetString` +// (e.g. "+01:00") is the host-zone offset at this wall-clock, used for the +// Local / Unspecified cases. +export function dateToStringWithCustomFormat( + info: DateInfo, format: string, kind: DateTimeKind, hostOffsetString: string): string { + let cursorPos = 0; + let tokenLength = 0; + let result = ""; + + while (cursorPos < format.length) { + const token = format[cursorPos]; + + switch (token) { + case "d": + tokenLength = parseRepeatToken(format, cursorPos, "d"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.day; + break; + case 2: + result += padWithZeros(info.day, 2); + break; + case 3: + result += shortDays[info.dayOfWeek]; + break; + case 4: + default: + result += longDays[info.dayOfWeek]; + break; + } + break; + case "f": + // The first `tokenLength` fractional-second digits, always padded. + tokenLength = parseRepeatToken(format, cursorPos, "f"); + cursorPos += tokenLength; + if (tokenLength <= 7) { + result += fractionDigits(info).substring(0, tokenLength); + } else { + throw "Input string was not in a correct format."; + } + break; + case "F": + // As "f", but with trailing zeros dropped — and nothing at all when the + // requested digits are all zero. + tokenLength = parseRepeatToken(format, cursorPos, "F"); + cursorPos += tokenLength; + if (tokenLength <= 7) { + result += fractionDigits(info).substring(0, tokenLength).replace(/0+$/, ""); + } else { + throw "Input string was not in a correct format."; + } + break; + case "g": + tokenLength = parseRepeatToken(format, cursorPos, "g"); + cursorPos += tokenLength; + result += "A.D."; + break; + case "h": + tokenLength = parseRepeatToken(format, cursorPos, "h"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + const h1Value = info.hour % 12; + result += h1Value ? h1Value : 12; + break; + case 2: + default: + const h2Value = info.hour % 12; + result += padWithZeros(h2Value ? h2Value : 12, 2); + break; + } + break; + case "H": + tokenLength = parseRepeatToken(format, cursorPos, "H"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.hour; + break; + case 2: + default: + result += padWithZeros(info.hour, 2); + break; + } + break; + case "K": + tokenLength = parseRepeatToken(format, cursorPos, "K"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + switch (kind) { + case DateTimeKind.Utc: + result += "Z"; + break; + case DateTimeKind.Local: + result += hostOffsetString; + break; + case DateTimeKind.Unspecified: + break; + } + break; + default: + break; + } + break; + case "m": + tokenLength = parseRepeatToken(format, cursorPos, "m"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.minute; + break; + case 2: + default: + result += padWithZeros(info.minute, 2); + break; + } + break; + case "M": + tokenLength = parseRepeatToken(format, cursorPos, "M"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.month; + break; + case 2: + result += padWithZeros(info.month, 2); + break; + case 3: + result += shortMonths[info.month - 1]; + break; + case 4: + default: + result += longMonths[info.month - 1]; + break; + } + break; + case "s": + tokenLength = parseRepeatToken(format, cursorPos, "s"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.second; + break; + case 2: + default: + result += padWithZeros(info.second, 2); + break; + } + break; + case "t": + tokenLength = parseRepeatToken(format, cursorPos, "t"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.hour < 12 ? "A" : "P"; + break; + case 2: + default: + result += info.hour < 12 ? "AM" : "PM"; + break; + } + break; + case "y": + tokenLength = parseRepeatToken(format, cursorPos, "y"); + cursorPos += tokenLength; + switch (tokenLength) { + case 1: + result += info.year % 100; + break; + case 2: + result += padWithZeros(info.year % 100, 2); + break; + default: + result += padWithZeros(info.year, tokenLength); + break; + } + break; + case "z": + tokenLength = parseRepeatToken(format, cursorPos, "z"); + cursorPos += tokenLength; + const utcOffsetText = kind === DateTimeKind.Utc ? "+00:00" : hostOffsetString; + + const sign = utcOffsetText[0] === "-" ? "-" : "+"; + const hours = parseInt(utcOffsetText.substring(1, 3), 10); + const minutes = parseInt(utcOffsetText.substring(4, 6), 10); + + switch (tokenLength) { + case 1: + result += `${sign}${hours}`; + break; + case 2: + result += `${sign}${padWithZeros(hours, 2)}`; + break; + default: + result += `${sign}${padWithZeros(hours, 2)}:${padWithZeros(minutes, 2)}`; + break; + } + break; + case ":": + result += ":"; + cursorPos++; + break; + case "/": + result += "/"; + cursorPos++; + break; + case "'": + case '"': + const [quotedString, quotedStringLenght] = parseQuotedString(format, cursorPos); + result += quotedString; + cursorPos += quotedStringLenght; + break; + case "%": + const nextChar = parseNextChar(format, cursorPos); + if (nextChar >= 0 && nextChar !== "%".charCodeAt(0)) { + cursorPos += 2; + result += dateToStringWithCustomFormat(info, String.fromCharCode(nextChar), kind, hostOffsetString); + } else { + throw new Exception("Invalid format string"); + } + break; + case "\\": + const nextChar2 = parseNextChar(format, cursorPos); + if (nextChar2 >= 0) { + cursorPos += 2; + result += String.fromCharCode(nextChar2); + } else { + throw new Exception("Invalid format string"); + } + break; + default: + cursorPos++; + result += token; + break; + } + } + + return result; +} + +// --- Standard single-letter format building blocks (InvariantCulture) --- + +export function dateToString_D(info: DateInfo): string { + return longDays[info.dayOfWeek] + + ", " + padWithZeros(info.day, 2) + + " " + longMonths[info.month - 1] + + " " + info.year; +} + +export function dateToString_d(info: DateInfo): string { + return padWithZeros(info.month, 2) + + "/" + padWithZeros(info.day, 2) + + "/" + info.year; +} + +export function dateToString_T(info: DateInfo): string { + return padWithZeros(info.hour, 2) + + ":" + padWithZeros(info.minute, 2) + + ":" + padWithZeros(info.second, 2); +} + +export function dateToString_t(info: DateInfo): string { + return padWithZeros(info.hour, 2) + + ":" + padWithZeros(info.minute, 2); +} + +// RFC 1123: "Thu, 01 Jan 2009 00:00:00 GMT". `info` must already be UTC. +export function dateToString_R(info: DateInfo): string { + return shortDays[info.dayOfWeek] + ", " + + padWithZeros(info.day, 2) + " " + + shortMonths[info.month - 1] + " " + + info.year + " " + + padWithZeros(info.hour, 2) + ":" + + padWithZeros(info.minute, 2) + ":" + + padWithZeros(info.second, 2) + " GMT"; +} + +// Universal sortable: "2009-06-15 13:45:30Z". `info` must already be UTC. +export function dateToString_u(info: DateInfo): string { + return padWithZeros(info.year, 4) + "-" + + padWithZeros(info.month, 2) + "-" + + padWithZeros(info.day, 2) + " " + + padWithZeros(info.hour, 2) + ":" + + padWithZeros(info.minute, 2) + ":" + + padWithZeros(info.second, 2) + "Z"; +} + +// Month/day (InvariantCulture "MMMM dd"): "June 15" +export function dateToString_M(info: DateInfo): string { + return longMonths[info.month - 1] + " " + padWithZeros(info.day, 2); +} + +// Year/month (InvariantCulture "yyyy MMMM"): "2009 June" +export function dateToString_Y(info: DateInfo): string { + return info.year + " " + longMonths[info.month - 1]; +} + +// Everything a standard format specifier needs beyond the wall-clock fields. +// DateTime and DateTimeOffset differ only in these, so they share the table below. +// Everything past `info` is a thunk: most specifiers need none of them, and each +// costs a time-zone conversion. +export interface FormatContext { + info: DateInfo; + utcInfo: () => DateInfo; + // "O"/"o" and "s" are ISO 8601, which Temporal renders itself — and renders + // tick-exactly — so the owning module supplies them rather than this file + // reassembling the fields by hand. + roundTrip: () => string; + sortable: () => string; + // Trails the no-format rendering. A DateTimeOffset shows its offset there; a + // DateTime has none to show. + defaultSuffix: string; + // Drive the K and z specifiers of a custom format. + kind: DateTimeKind; + hostOffsetString: () => string; +} + +// .NET's standard date/time format specifiers, InvariantCulture. Anything that is +// not a single recognised letter is treated as a custom format string. +export function dateToString(ctx: FormatContext, format?: string): string { + const info = ctx.info; + + if (typeof format !== "string") { + return dateToString_d(info) + " " + dateToString_T(info) + ctx.defaultSuffix; + } + + if (format.length === 1) { + switch (format) { + case "D": return dateToString_D(info); + case "d": return dateToString_d(info); + case "F": return dateToString_D(info) + " " + dateToString_T(info); + case "f": return dateToString_D(info) + " " + dateToString_t(info); + case "G": return dateToString_d(info) + " " + dateToString_T(info); + case "g": return dateToString_d(info) + " " + dateToString_t(info); + case "M": case "m": return dateToString_M(info); + case "O": case "o": return ctx.roundTrip(); + case "R": case "r": return dateToString_R(ctx.utcInfo()); + case "s": return ctx.sortable(); + case "T": return dateToString_T(info); + case "t": return dateToString_t(info); + case "u": return dateToString_u(ctx.utcInfo()); + case "U": { + const utc = ctx.utcInfo(); + return dateToString_D(utc) + " " + dateToString_T(utc); + } + case "Y": case "y": return dateToString_Y(info); + default: throw new Exception("Unrecognized Date print format"); + } + } + + return dateToStringWithCustomFormat(info, format, ctx.kind, ctx.hostOffsetString()); +} + +// The only date words .NET's invariant parser recognises: month names, weekday names, +// meridiem designators and zone markers (plus the ISO "T" separator). Anything else is +// rejected. Used to reject JS-permissive inputs (see `parseRaw`). +const recognizedDateWords = new Set([ + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december", + "jan", "feb", "mar", "apr", "jun", "jul", "aug", "sep", "sept", "oct", "nov", "dec", + "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", + "mon", "tue", "wed", "thu", "fri", "sat", "sun", + "am", "pm", "gmt", "utc", "ut", "t", "z", +]); + +// The ticks below the millisecond (0..9999) written in the seconds fraction of +// `input`, e.g. "…:30.6175425" -> 5425. +// +// `parseRaw` resolves the instant through the JS `Date` built-in, which is +// millisecond-precise, so those digits would otherwise be lost. Anchoring on +// ":ss." keeps date separators (as in "15.06.2009") from matching. Digits past +// the 7th are truncated, matching how TimeOnly/TimeSpan parsing treats them. +export function subMillisecondTicks(input: string): number { + const m = /:\d{1,2}\.(\d+)/.exec(input); + return m == null ? 0 : +m[1].padEnd(7, "0").substring(3, 7); +} + +// A trailing UTC designator, if the input carries one. The JS `Date` built-in +// applies the offset but does not report whether there was one, and callers need +// that: .NET gives a parsed value Kind=Local when an offset is present and +// Kind=Unspecified when it is not. +// +// The offset is only recognised after a time, so that the trailing "-11" of a +// date like "2014-09-11" is not mistaken for one. +function detectOffset(input: string): Offset { + const m = /\d\d:\d\d(?::\d\d)?(?:\.\d+)?\s*(Z|[+-]\d\d:?(?:\d\d)?)\s*$/i.exec(input); + if (m == null) { + return null; + } + + const designator = m[1]; + if (designator.toUpperCase() === "Z") { + return "Z"; + } + + const digits = designator.substring(1).replace(":", ""); + const minutes = +digits.substring(0, 2) * 60 + (digits.length > 2 ? +digits.substring(2) : 0); + return designator[0] === "-" ? -minutes : minutes; +} + +// Lenient .NET-compatible parse. Returns a JS Date (the parsed instant) and the +// detected offset, if any. Callers read the wall-clock fields and build the +// appropriate Temporal value. +export function parseRaw(input: string): [Date, Offset] { + function fail() { + throw new Exception(`The string is not a valid Date: ${input}`); + } + + if (input == null || input.trim() === "") { + fail(); + } + + if ((input.match(/[a-z]+/gi) ?? []).some(word => !recognizedDateWords.has(word.toLowerCase()))) { + fail(); + } + + // ISO dates without TZ are parsed as UTC. Adding time without TZ keeps them local. + if (input.length === 10 && input[4] === "-" && input[7] === "-") { + input += "T00:00:00"; + } + let date = new Date(input); + let offset: Offset = detectOffset(input); + + if (isNaN(date.getTime())) { + // Try to check strings JS Date cannot parse (see #1045, #1422) + const m = /^\s*(\d+[^\w\s:]\d+[^\w\s:]\d+)?\s*(\d+:\d+(?::\d+(?:\.\d+)?)?)?\s*([AaPp][Mm])?\s*(Z|[+-]([01]?\d):?([0-5]?\d)?)?\s*$/.exec(input); + if (m != null) { + let baseDate: Date; + let timeInSeconds = 0; + if (m[2] != null) { + const timeParts = m[2].split(":"); + const hourPart = parseInt(timeParts[0], 10); + timeInSeconds = + hourPart * 3600 + + parseInt(timeParts[1] || "0", 10) * 60 + + parseFloat(timeParts[2] || "0"); + if (m[3] != null && m[3].toUpperCase() === "PM" && hourPart < 12) { + timeInSeconds += 12 * 3600; + } else if (m[3] != null && m[3].toUpperCase() === "AM" && hourPart === 12) { + timeInSeconds -= 12 * 3600; + } + } + if (m[4] != null) { // There's an offset, parse as UTC + if (m[1] != null) { + baseDate = new Date(m[1] + " UTC"); + } else { + const d = new Date(); + baseDate = new Date(d.getUTCFullYear() + "/" + (d.getUTCMonth() + 1) + "/" + d.getUTCDate()); + } + if (m[4] === "Z") { + offset = "Z"; + } else { + let offsetInMinutes = parseInt(m[5], 10) * 60 + parseInt(m[6] || "0", 10); + if (m[4][0] === "-") { + offsetInMinutes *= -1; + } + offset = offsetInMinutes; + timeInSeconds -= offsetInMinutes * 60; + } + } else { + if (m[1] != null) { + baseDate = new Date(m[1]); + } else { + const d = new Date(); + baseDate = new Date(d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate()); + } + } + date = new Date(baseDate.getTime() + timeInSeconds * 1000); + // correct for daylight savings time + date = new Date(date.getTime() + (date.getTimezoneOffset() - baseDate.getTimezoneOffset()) * 60_000); + } else { + fail(); + } + + // Check again the date is valid after transformations, see #2229 + if (isNaN(date.getTime())) { + fail(); + } + } + + return [date, offset]; +} diff --git a/src/fable-library-ts/DateTimeOffsetTemporal.ts b/src/fable-library-ts/DateTimeOffsetTemporal.ts new file mode 100644 index 0000000000..230b7de4cd --- /dev/null +++ b/src/fable-library-ts/DateTimeOffsetTemporal.ts @@ -0,0 +1,355 @@ +/** + * DateTimeOffset as an offset-only Temporal.ZonedDateTime. + * + * .NET DateTimeOffset is a wall-clock date+time plus an explicit UTC offset (a + * TimeSpan of whole minutes). A ZonedDateTime whose time zone is a fixed offset + * (e.g. "-08:00") carries the wall-clock, the offset, and the absolute instant + * natively — so there is no extra metadata to stamp and no re-stamping after + * operations (arithmetic returns a ZonedDateTime that keeps the offset). + * + * The offset is exchanged with the rest of the runtime as a TimeSpan (Temporal + * .Duration), matching how .NET types it. Equality/comparison are by instant + * (offset-independent), matching .NET '=='; EqualsExact also compares the offset. + * + * Formatting and parsing run on the Temporal value's own fields (see + * DateTimeFormat.ts) rather than through a JS Date, which would truncate every + * value to milliseconds. + */ + +import { int64, fromFloat64 } from "./BigInt.ts"; +import { FSharpRef } from "./Types.ts"; +import { Exception, DateTimeKind, padWithZeros } from "./Util.ts"; +import { + TimeSpan, fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, + totalNanoseconds as TimeSpan_totalNanoseconds, +} from "./TimeSpanTemporal.ts"; +import { DateTime, dateTime, create as createDateTime, getTicks as DateTime_getTicks } from "./DateTimeTemporal.ts"; +import * as Format from "./DateTimeFormat.ts"; + +export type DateTimeOffset = Temporal.ZonedDateTime; + +export const ZonedDateTime = Temporal.ZonedDateTime; +export type ZonedDateTime = Temporal.ZonedDateTime; + +// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods. +const proto = Temporal.ZonedDateTime.prototype as any; +proto.Equals = function (this: DateTimeOffset, other: DateTimeOffset): boolean { return equals(this, other); }; +proto.CompareTo = function (this: DateTimeOffset, other: DateTimeOffset): number { return compare(this, other); }; +proto.GetHashCode = function (this: DateTimeOffset): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateTimeOffset, format?: string): string { return toString(this, format); }; + +const nsPerMinute = 60_000_000_000n; + +// Nanoseconds between the Unix epoch (1970-01-01) and DateTime.MinValue (0001-01-01) +const epochToMinValueNs = 62_135_596_800_000n * 1_000_000n; + +function hostTimeZone(): string { + return Temporal.Now.timeZoneId(); +} + +// .NET DateTimeOffset has 100ns (tick) precision; Temporal instants are +// nanosecond-precise. Floors, so the tick count from year 1 is floored too. +function truncateToTicks(epochNs: bigint): bigint { + const remainder = epochNs % 100n; + return remainder >= 0n ? epochNs - remainder : epochNs - remainder - 100n; +} + +function checkOffsetInRange(offsetNs: bigint) { + if (offsetNs % nsPerMinute !== 0n) { + throw new Exception("Offset must be specified in whole minutes."); + } + if (offsetNs > 14n * 60n * nsPerMinute || offsetNs < -14n * 60n * nsPerMinute) { + throw new Exception("Offset must be within plus or minus 14 hours."); + } +} + +function offsetNanosecondsToZoneString(offsetNs: bigint): string { + const sign = offsetNs < 0n ? "-" : "+"; + const abs = offsetNs < 0n ? -offsetNs : offsetNs; + const totalMinutes = Number(abs / nsPerMinute); + return `${sign}${padWithZeros(Math.trunc(totalMinutes / 60), 2)}:${padWithZeros(totalMinutes % 60, 2)}`; +} + +function offsetToNanoseconds(offset: TimeSpan): bigint { + return TimeSpan_totalNanoseconds(offset); +} + +function fromEpoch(epochNs: bigint, offsetNs: bigint = 0n): DateTimeOffset { + if (offsetNs === 0n) + return new Temporal.ZonedDateTime(epochNs, "UTC"); + + checkOffsetInRange(offsetNs); + return new Temporal.ZonedDateTime(epochNs, offsetNanosecondsToZoneString(offsetNs)); +} + +export function offset(d: DateTimeOffset): TimeSpan { + return TimeSpan_fromTicks(BigInt(d.offsetNanoseconds) / 100n); +} + +export function create( + year: number, month: number, day: number, + h: number, m: number, s: number, + ms: number | TimeSpan, offset?: TimeSpan): DateTimeOffset { + // Overload without milliseconds: the 7th argument is the offset + if (offset == null) { + offset = ms as TimeSpan; + ms = 0; + } + const offsetNs = offsetToNanoseconds(offset); + checkOffsetInRange(offsetNs); + return Temporal.ZonedDateTime.from({ + year, month, day, hour: h, minute: m, second: s, millisecond: ms as number, + timeZone: offsetNanosecondsToZoneString(offsetNs), + }); +} + +// Offset (ns) of the host time zone for a given wall-clock, DST-aware. Resolved +// through Temporal rather than `new Date(year, ...)`, which maps years 0-99 into +// 1900-1999 and would pick that era's rules. +function hostOffsetNanoseconds(d: DateTime): bigint { + return BigInt(d.toZonedDateTime(hostTimeZone()).offsetNanoseconds); +} + +export function fromDate(date: DateTime, offset?: TimeSpan): DateTimeOffset { + const kind = date.kind ?? DateTimeKind.Unspecified; + let offsetNs: bigint; + switch (kind) { + case DateTimeKind.Utc: + if (offset != null && offsetToNanoseconds(offset) !== 0n) { + throw new Exception("The UTC Offset for Utc DateTime instances must be 0."); + } + offsetNs = 0n; + break; + case DateTimeKind.Local: + offsetNs = hostOffsetNanoseconds(date); + if (offset != null && offsetToNanoseconds(offset) !== offsetNs) { + throw new Exception("The UTC Offset of the local dateTime parameter does not match the offset argument."); + } + break; + default: + offsetNs = offset != null ? offsetToNanoseconds(offset) : hostOffsetNanoseconds(date); + break; + } + checkOffsetInRange(offsetNs); + return Temporal.ZonedDateTime.from({ + year: date.year, month: date.month, day: date.day, + hour: date.hour, minute: date.minute, second: date.second, + millisecond: date.millisecond, microsecond: date.microsecond, nanosecond: date.nanosecond, + timeZone: offsetNanosecondsToZoneString(offsetNs), + }); +} + +export function fromTicks(ticks: int64, offset: TimeSpan): DateTimeOffset { + const offsetNs = offsetToNanoseconds(offset); + // ticks are the local wall-clock ticks; the instant is that minus the offset + const localNs = BigInt(ticks) * 100n - epochToMinValueNs; + return fromEpoch(localNs - offsetNs, offsetNs); +} + +export function fromUnixTimeMilliseconds(ms: int64): DateTimeOffset { + return fromEpoch(BigInt(ms) * 1_000_000n); +} + +export function fromUnixTimeSeconds(seconds: int64): DateTimeOffset { + return fromEpoch(BigInt(seconds) * 1_000_000_000n); +} + +export function minValue(): DateTimeOffset { + return fromEpoch(-epochToMinValueNs); +} + +export function maxValue(): DateTimeOffset { + // 9999-12-31T23:59:59.9999999 UTC + return fromEpoch(253_402_300_799_999_999_900n, 0n); +} + +export function dayOfWeek(d: DateTimeOffset): number { + return d.dayOfWeek % 7; +} + +export function timeOfDay(d: DateTimeOffset): TimeSpan { + // Elapsed since midnight (local wall-clock). DateTimeOffset values are tick-aligned, so this is too. + return d.startOfDay().until(d); +} + +export function date(d: DateTimeOffset): DateTime { + return createDateTime(d.year, d.month, d.day, 0, 0, 0, 0, DateTimeKind.Unspecified); +} + +export function dateTimeProp(d: DateTimeOffset): DateTime { + return dateTime(d.toPlainDateTime(), DateTimeKind.Unspecified); +} + +export function toUniversalTime(d: DateTimeOffset): DateTimeOffset { + return fromEpoch(d.epochNanoseconds); +} + +export function toLocalTime(d: DateTimeOffset): DateTimeOffset { + const local = d.withTimeZone(hostTimeZone()); + return fromEpoch(local.epochNanoseconds, BigInt(local.offsetNanoseconds)); +} + +export function utcDateTime(d: DateTimeOffset): DateTime { + return dateTime(d.withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc); +} + +export function localDateTime(d: DateTimeOffset): DateTime { + return dateTime(d.withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local); +} + +export function add(d: DateTimeOffset, ts: TimeSpan): DateTimeOffset { + return d.add(ts); +} + +export function addYears(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add({ years: v }); +} + +export function addMonths(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add({ months: v }); +} + +export function addDays(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "days")); +} + +export function addHours(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "hours")); +} + +export function addMinutes(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "minutes")); +} + +export function addSeconds(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "seconds")); +} + +export function addMilliseconds(d: DateTimeOffset, v: number): DateTimeOffset { + return d.add(TimeSpan_fromUnits(v, "milliseconds")); +} + +export function addTicks(d: DateTimeOffset, v: int64): DateTimeOffset { + return d.add(TimeSpan_fromTicks(v)); +} + +// Split rather than returning a union — see the note in DateTimeTemporal.ts. +export function subtractDate(d: DateTimeOffset, that: DateTimeOffset): TimeSpan { + return TimeSpan_fromTicks((d.epochNanoseconds - that.epochNanoseconds) / 100n); // instant difference +} + +export function subtractTimeSpan(d: DateTimeOffset, ts: TimeSpan): DateTimeOffset { + return d.subtract(ts); +} + +export function equals(d1: DateTimeOffset, d2: DateTimeOffset): boolean { + return d1.epochNanoseconds === d2.epochNanoseconds; +} + +export function equalsExact(d1: DateTimeOffset, d2: DateTimeOffset): boolean { + return d1.epochNanoseconds === d2.epochNanoseconds && d1.offsetNanoseconds === d2.offsetNanoseconds; +} + +export function compare(d1: DateTimeOffset, d2: DateTimeOffset): number { + const a = d1.epochNanoseconds, b = d2.epochNanoseconds; + return a < b ? -1 : a > b ? 1 : 0; +} + +export const compareTo = compare; + +export function op_Addition(x: DateTimeOffset, y: TimeSpan): DateTimeOffset { + return add(x, y); +} + +export function hash(d: DateTimeOffset): number { + return Number(d.epochNanoseconds % 2147483647n); +} + +export function toOffset(d: DateTimeOffset, offset: TimeSpan): DateTimeOffset { + return fromEpoch(d.epochNanoseconds, offsetToNanoseconds(offset)); +} + +export function getUtcTicks(d: DateTimeOffset): int64 { + return DateTime_getTicks(utcDateTime(d)); +} + +export function getTicks(d: DateTimeOffset): int64 { + return DateTime_getTicks(dateTimeProp(d)); +} + +export function toUnixTimeMilliseconds(d: DateTimeOffset): int64 { + return fromFloat64(d.epochMilliseconds); +} + +export function toUnixTimeSeconds(d: DateTimeOffset): int64 { + return fromFloat64(Math.floor(d.epochMilliseconds / 1000)); +} + +export function now(): DateTimeOffset { + const z = Temporal.Now.zonedDateTimeISO(hostTimeZone()); + return fromEpoch(truncateToTicks(z.epochNanoseconds), BigInt(z.offsetNanoseconds)); +} + +export function utcNow(): DateTimeOffset { + return fromEpoch(truncateToTicks(Temporal.Now.instant().epochNanoseconds)); +} + +// --- Formatting and parsing --- + +// Wall-clock fields for the formatter, with the sub-second component in .NET +// ticks so "O" and the f/F specifiers can print all 7 digits. +function toDateInfo(z: DateTimeOffset): Format.DateInfo { + return { + year: z.year, + month: z.month, + day: z.day, + hour: z.hour, + minute: z.minute, + second: z.second, + tick: z.millisecond * 10_000 + z.microsecond * 10 + Math.trunc(z.nanosecond / 100), + dayOfWeek: z.dayOfWeek % 7, + }; +} + +export function toString(d: DateTimeOffset, format?: string, _provider?: any): string { + const offsetString = () => offsetNanosecondsToZoneString(BigInt(d.offsetNanoseconds)); + + return Format.dateToString( + { + info: toDateInfo(d), + utcInfo: () => toDateInfo(d.withTimeZone("UTC")), + // A fixed-offset zone renders as .NET's "O" exactly: ISO wall-clock plus + // the numeric offset, with no [zone] annotation. + roundTrip: () => d.toString({ fractionalSecondDigits: 7, timeZoneName: "never" }), + sortable: () => d.toPlainDateTime().toString({ smallestUnit: "second" }), + defaultSuffix: " " + offsetString(), + // K/z print the value's own offset, never the host zone's, so Local is + // passed as the kind and that offset stands in for the host one. + kind: DateTimeKind.Local, + hostOffsetString: offsetString, + }, + format); +} + +export function parse(str: string): DateTimeOffset { + const [parsed, offsetMatch] = Format.parseRaw(str); + // parseRaw resolves the instant through JS Date, which truncates at the + // millisecond, so the digits below that are recovered from the input separately. + const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n; + + const offsetNs = offsetMatch == null + ? BigInt(new Temporal.Instant(epochNs).toZonedDateTimeISO(hostTimeZone()).offsetNanoseconds) + : (offsetMatch === "Z" ? 0n : BigInt(offsetMatch) * nsPerMinute); + + return fromEpoch(epochNs, offsetNs); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/DateTimeTemporal.ts b/src/fable-library-ts/DateTimeTemporal.ts new file mode 100644 index 0000000000..0faab16ffa --- /dev/null +++ b/src/fable-library-ts/DateTimeTemporal.ts @@ -0,0 +1,313 @@ +/** + * DateTime as an extended Temporal.PlainDateTime. + * + * .NET DateTime is a wall-clock date+time (no offset) plus a Kind (Utc | Local | + * Unspecified) that is metadata only. PlainDateTime models the wall-clock exactly + * and, unlike the JS Date representation, is tick (100ns) precise and DST-agnostic + * for arithmetic — which matches .NET, where DateTime math operates on the raw + * wall-clock ticks. + * + * Kind has no Temporal home, so it is attached to the instance as a `kind` property + * (Temporal objects are extensible). Because Temporal operations return fresh, + * un-stamped instances, every operation must funnel through `dateTime(...)` to + * re-stamp the kind. + * + * Formatting, parsing and time-zone conversion are done on the Temporal value + * itself (see DateTimeFormat.ts), never by round-tripping through a JS Date — + * that would silently truncate every value to milliseconds. The one exception is + * the four ToLong/ShortDate/TimeString members, which are host-locale display + * strings with no .NET-defined layout and no sub-second component. + */ + +import { int64, toInt64 } from "./BigInt.ts"; +import { FSharpRef } from "./Types.ts"; +import { DateTimeKind } from "./Util.ts"; +import { + TimeSpan, fromTicks as TimeSpan_fromTicks, fromUnits as TimeSpan_fromUnits, + totalNanoseconds as TimeSpan_totalNanoseconds, +} from "./TimeSpanTemporal.ts"; +import * as Format from "./DateTimeFormat.ts"; + +export type DateTime = Temporal.PlainDateTime & { kind?: DateTimeKind }; + +export const PlainDateTime = Temporal.PlainDateTime; +export type PlainDateTime = Temporal.PlainDateTime; + +// Generic equality/comparison/hashing dispatch (Util.ts) uses .NET-style methods. +const proto = Temporal.PlainDateTime.prototype as any; +proto.Equals = function (this: DateTime, other: DateTime): boolean { return Temporal.PlainDateTime.compare(this, other) === 0; }; +proto.CompareTo = function (this: DateTime, other: DateTime): number { return Temporal.PlainDateTime.compare(this, other); }; +proto.GetHashCode = function (this: DateTime): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: DateTime, format?: string): string { return toString(this, format); }; + +const minDateTime = new Temporal.PlainDateTime(1, 1, 1); + +export function getKind(value: DateTime): DateTimeKind { + return value.kind ?? DateTimeKind.Unspecified; +} + +export function dateTime(value: Temporal.PlainDateTime, kind: DateTimeKind = DateTimeKind.Unspecified): DateTime { + const d = value as DateTime; + d.kind = kind; + return d; +} + +// `dateTime` stamps in place, so anything handing out a value derived from a +// shared instance (a module constant, or the argument of SpecifyKind) has to +// copy first, or it would mutate the caller's value. +function copy(d: Temporal.PlainDateTime): Temporal.PlainDateTime { + return new Temporal.PlainDateTime( + d.year, d.month, d.day, d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond); +} + +function hostTimeZone(): string { + return Temporal.Now.timeZoneId(); +} + +// Host-zone offset at this wall-clock, e.g. "+01:00". Unlike building a JS Date +// from the fields, this is correct for years 0-99 (which JS maps to 1900-1999). +function hostOffsetString(d: Temporal.PlainDateTime): string { + return d.toZonedDateTime(hostTimeZone()).offset; +} + +// Wall-clock fields for the formatter. The sub-second component is carried in +// .NET ticks so that "O" and the f/F specifiers can print all 7 digits. +function toDateInfo(d: DateTime): Format.DateInfo { + return { + year: d.year, + month: d.month, + day: d.day, + hour: d.hour, + minute: d.minute, + second: d.second, + tick: d.millisecond * 10_000 + d.microsecond * 10 + Math.trunc(d.nanosecond / 100), + dayOfWeek: dayOfWeek(d), + }; +} + +// Display-only bridge for the host-locale strings below. Sub-second precision is +// irrelevant to all four, but years 0-99 still need correcting. +function toJsDate(d: DateTime): Date { + const jsDate = new Date(d.year, d.month - 1, d.day, d.hour, d.minute, d.second, d.millisecond); + jsDate.setFullYear(d.year, d.month - 1, d.day); + return jsDate; +} + +export function create( + year: number, month: number, day: number, + h: number = 0, m: number = 0, s: number = 0, + ms: number = 0, kind?: DateTimeKind): DateTime { + return dateTime(new Temporal.PlainDateTime(year, month, day, h, m, s, ms), kind); +} + +export function fromTicks(ticks: number | bigint, kind?: DateTimeKind): DateTime { + return dateTime(minDateTime.add(TimeSpan_fromTicks(ticks)), kind); +} + +export function getTicks(date: DateTime): int64 { + return toInt64(TimeSpan_totalNanoseconds(minDateTime.until(date, { largestUnit: "day" })) / 100n); +} + +export function minValue(): DateTime { + return dateTime(copy(minDateTime), DateTimeKind.Unspecified); +} + +export function maxValue(): DateTime { + return dateTime(new Temporal.PlainDateTime(9999, 12, 31, 23, 59, 59, 999, 999, 900), DateTimeKind.Unspecified); +} + +// .NET DateTime has 100ns (tick) precision; Temporal.Now is nanosecond-precise. +function truncateToTicks(d: Temporal.PlainDateTime): Temporal.PlainDateTime { + return d.round({ smallestUnit: "nanosecond", roundingIncrement: 100, roundingMode: "trunc" }); +} + +export function now(): DateTime { + return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO()), DateTimeKind.Local); +} + +export function utcNow(): DateTime { + return dateTime(truncateToTicks(Temporal.Now.plainDateTimeISO("UTC")), DateTimeKind.Utc); +} + +export function today(): DateTime { + return date(now()); +} + +export function specifyKind(d: DateTime, kind: DateTimeKind): DateTime { + // Kind is metadata: every tick of the wall-clock is preserved. + return dateTime(copy(d), kind); +} + +export function dayOfWeek(d: DateTime): number { + // Temporal: Monday = 1 ... Sunday = 7, .NET: Sunday = 0 ... Saturday = 6 + return d.dayOfWeek % 7; +} + +export function date(d: DateTime): DateTime { + return dateTime(d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }), getKind(d)); +} + +export function timeOfDay(d: DateTime): TimeSpan { + // Elapsed since midnight. DateTime values are tick-aligned, so this is too. + return d.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 }).until(d); +} + +export function add(d: DateTime, ts: TimeSpan): DateTime { + return dateTime(d.add(ts), getKind(d)); +} + +export function addYears(d: DateTime, v: number): DateTime { + return dateTime(d.add({ years: v }), getKind(d)); +} + +export function addMonths(d: DateTime, v: number): DateTime { + return dateTime(d.add({ months: v }), getKind(d)); +} + +export function addDays(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "days")), getKind(d)); +} + +export function addHours(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "hours")), getKind(d)); +} + +export function addMinutes(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "minutes")), getKind(d)); +} + +export function addSeconds(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "seconds")), getKind(d)); +} + +export function addMilliseconds(d: DateTime, v: number): DateTime { + return dateTime(d.add(TimeSpan_fromUnits(v, "milliseconds")), getKind(d)); +} + +export function addTicks(d: DateTime, v: int64): DateTime { + return dateTime(d.add(TimeSpan_fromTicks(v)), getKind(d)); +} + +export function subtractDate(d: DateTime, that: DateTime): TimeSpan { + return that.until(d, { largestUnit: "day" }); +} + +export function subtractTimeSpan(d: DateTime, ts: TimeSpan): DateTime { + return dateTime(d.subtract(ts), getKind(d)); +} + +export function equals(d1: DateTime, d2: DateTime): boolean { + return Temporal.PlainDateTime.compare(d1, d2) === 0; +} + +export function compare(d1: DateTime, d2: DateTime): number { + return Temporal.PlainDateTime.compare(d1, d2); +} + +export const compareTo = compare; + +export function op_Addition(x: DateTime, y: TimeSpan): DateTime { + return add(x, y); +} + +export function hash(d: DateTime): number { + return Number(getTicks(d) % 2147483647n); +} + +export function isLeapYear(year: number): boolean { + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; +} + +export function daysInMonth(year: number, month: number): number { + return month === 2 + ? (isLeapYear(year) ? 29 : 28) + : (month >= 8 ? (month % 2 === 0 ? 31 : 30) : (month % 2 === 0 ? 30 : 31)); +} + +// --- Host time zone --- + +export function toUniversalTime(d: DateTime): DateTime { + // .NET reads an Unspecified value as local time here. + return getKind(d) === DateTimeKind.Utc + ? d + : dateTime(d.toZonedDateTime(hostTimeZone()).withTimeZone("UTC").toPlainDateTime(), DateTimeKind.Utc); +} + +export function toLocalTime(d: DateTime): DateTime { + // .NET reads an Unspecified value as UTC here — the mirror image of the above. + return getKind(d) === DateTimeKind.Local + ? d + : dateTime(d.toZonedDateTime("UTC").withTimeZone(hostTimeZone()).toPlainDateTime(), DateTimeKind.Local); +} + +export function isDaylightSavingTime(d: DateTime): boolean { + const tz = hostTimeZone(); + const offsetAt = (month: number) => new Temporal.PlainDateTime(d.year, month, 1).toZonedDateTime(tz).offsetNanoseconds; + // The larger of the two mid-season offsets is the daylight-saving one. + return Math.max(offsetAt(1), offsetAt(7)) === d.toZonedDateTime(tz).offsetNanoseconds; +} + +// --- Host-locale display strings (no .NET-defined layout) --- + +export function toLongDateString(d: DateTime): string { + return toJsDate(d).toDateString(); +} + +export function toShortDateString(d: DateTime): string { + return toJsDate(d).toLocaleDateString(); +} + +export function toLongTimeString(d: DateTime): string { + return toJsDate(d).toLocaleTimeString(); +} + +export function toShortTimeString(d: DateTime): string { + return toJsDate(d).toLocaleTimeString().replace(/:\d\d(?!:)/, ""); +} + +// --- Formatting and parsing --- + +export function toString(d: DateTime, format?: string, _provider?: any): string { + const kind = getKind(d); + + return Format.dateToString( + { + info: toDateInfo(d), + utcInfo: () => toDateInfo(toUniversalTime(d)), + // An Unspecified DateTime prints no zone at all, which is what makes "O" + // round-trip it back to Unspecified. + roundTrip: () => d.toString({ fractionalSecondDigits: 7 }) + + (kind === DateTimeKind.Utc ? "Z" : kind === DateTimeKind.Local ? hostOffsetString(d) : ""), + sortable: () => d.toString({ smallestUnit: "second" }), + defaultSuffix: "", + kind, + hostOffsetString: () => hostOffsetString(d), + }, + format); +} + +export function parse(str: string, detectUTC = false): DateTime { + const [parsed, offset] = Format.parseRaw(str); + // .NET always parses DateTime as Local if there's offset info (even "Z") + // Newtonsoft.Json uses UTC if the offset is "Z" + const kind = offset != null + ? (detectUTC && offset === "Z" ? DateTimeKind.Utc : DateTimeKind.Local) + : DateTimeKind.Unspecified; + + // parseRaw resolves the instant through JS Date, which truncates at the + // millisecond, so the digits below that are recovered from the input separately. + const epochNs = BigInt(parsed.getTime()) * 1_000_000n + BigInt(Format.subMillisecondTicks(str)) * 100n; + const zone = kind === DateTimeKind.Utc ? "UTC" : hostTimeZone(); + + return dateTime(new Temporal.Instant(epochNs).toZonedDateTimeISO(zone).toPlainDateTime(), kind); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/Guid.ts b/src/fable-library-ts/Guid.ts index e74d2011a0..009c2350bf 100644 --- a/src/fable-library-ts/Guid.ts +++ b/src/fable-library-ts/Guid.ts @@ -73,8 +73,10 @@ export function newGuid() { } // RFC 9562 UUID v7 -export function createVersion7(timestamp?: Date): string { - const ms = timestamp != null ? timestamp.getTime() : Date.now(); +export function createVersion7(timestamp?: Date | number | bigint): string { + // A number/bigint timestamp is Unix milliseconds (used by the Temporal representation, + // whose DateTimeOffset is not a JS Date); a Date uses getTime(). + const ms = timestamp == null ? Date.now() : typeof timestamp === "object" ? timestamp.getTime() : Number(timestamp); // 48-bit timestamp as hex const msHex = Math.floor(ms).toString(16).padStart(12, "0"); diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 03d259c7bd..193d2d9138 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -4,6 +4,20 @@ import { escape } from "./RegExp.ts"; import { toString } from "./Types.ts"; import { Exception } from "./Util.ts"; +// Temporal date/time values (only present under --test:js-temporal) are not JS Dates. To avoid +// importing the side-effecting Temporal runtime modules into this core file — which would pull +// them into every bundle — they advertise a .NET-style formatter through this well-known symbol, +// attached to their prototype when (and only when) those modules are loaded. +const dateTimeFormattableSymbol = Symbol.for("Fable.DateTimeFormattable"); + +function isNetDateFormattable(rep: any): boolean { + return rep != null && typeof rep[dateTimeFormattableSymbol] === "function"; +} + +function formatDateLike(rep: any, format: string | undefined): string { + return rep instanceof Date ? dateToString(rep, format) : rep[dateTimeFormattableSymbol](format); +} + const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\*|\d+)?(?:\.(\d+))?(\w)/g; const interpolateRegExp = /(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)/g; const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; @@ -251,8 +265,8 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, rep = String(rep); break; } - } else if (rep instanceof Date) { - rep = dateToString(rep); + } else if (rep instanceof Date || isNetDateFormattable(rep)) { + rep = formatDateLike(rep, undefined); } else if (format === "A" && typeof rep === "string") { rep = "\"" + rep + "\""; } else { @@ -513,8 +527,8 @@ export function format(str: string | object, ...args: any[]) { rep = sign + rep; } } - } else if (rep instanceof Date) { - rep = dateToString(rep, pattern || format); + } else if (rep instanceof Date || isNetDateFormattable(rep)) { + rep = formatDateLike(rep, pattern || format); } else { rep = toString(rep) } diff --git a/src/fable-library-ts/TimeOnlyTemporal.ts b/src/fable-library-ts/TimeOnlyTemporal.ts new file mode 100644 index 0000000000..0ec7a8005b --- /dev/null +++ b/src/fable-library-ts/TimeOnlyTemporal.ts @@ -0,0 +1,181 @@ +import { FSharpRef } from "./Types.ts"; +import { toInt64, fromFloat64, int64 } from "./BigInt.ts"; +import { TimeSpan, fromTicks as TimeSpan_fromTicks, totalNanoseconds as TimeSpan_totalNanoseconds } from "./TimeSpanTemporal.ts"; +import { Exception } from "./Util.ts"; + +export type TimeOnly = Temporal.PlainTime; +export const PlainTime = Temporal.PlainTime; +export type PlainTime = Temporal.PlainTime; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods. Attach them so TimeOnly +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.PlainTime.prototype as any; +proto.Equals = function (this: TimeOnly, other: TimeOnly): boolean { return this.equals(other); }; +proto.CompareTo = function (this: TimeOnly, other: TimeOnly): number { return Temporal.PlainTime.compare(this, other); }; +proto.GetHashCode = function (this: TimeOnly): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: TimeOnly, format?: string): string { return toString(this, format); }; + +const nanosecondsPerDay = 86_400_000_000_000; +const nanosecondsPerDayBig = 86_400_000_000_000n; + +function totalNanoseconds(t: TimeOnly): number { + return ((t.hour * 60 + t.minute) * 60 + t.second) * 1_000_000_000 + + t.millisecond * 1_000_000 + t.microsecond * 1_000 + t.nanosecond; +} + +function fromNanoseconds(n: number): TimeOnly { + return new Temporal.PlainTime( + Math.floor(n / 3_600_000_000_000), + Math.floor(n / 60_000_000_000) % 60, + Math.floor(n / 1_000_000_000) % 60, + Math.floor(n / 1_000_000) % 1000, + Math.floor(n / 1_000) % 1000, + n % 1000); +} + +export function create(h: number = 0, m: number = 0, s: number = 0, ms: number = 0, us: number = 0): TimeOnly { + return new Temporal.PlainTime(h, m, s, ms, us); +} + +export function fromTicks(ticks: number | bigint): TimeOnly { + return fromNanoseconds(Number(BigInt(ticks) * 100n)); +} + +export function fromTimeSpan(timeSpan: TimeSpan): TimeOnly { + const ns = Number(TimeSpan_totalNanoseconds(timeSpan)); + if (ns < 0 || ns >= nanosecondsPerDay) + throw new Exception("The TimeSpan describes an unrepresentable TimeOnly."); + + return fromNanoseconds(ns); +} + +export function fromDateTime(d: Temporal.PlainDateTime): TimeOnly { + // Under the Temporal representation a DateTime is a PlainDateTime (kind-agnostic wall-clock) + return new Temporal.PlainTime(d.hour, d.minute, d.second, d.millisecond, d.microsecond, d.nanosecond); +} + +export function minValue(): TimeOnly { + return new Temporal.PlainTime(); +} + +export function maxValue(): TimeOnly { + // This is "23:59:59.9999999" (.NET tick precision) + return new Temporal.PlainTime(23, 59, 59, 999, 999, 900); +} + +export function ticks(t: TimeOnly): int64 { + return toInt64(fromFloat64(totalNanoseconds(t) / 100)); +} + +export function toTimeSpan(t: TimeOnly): TimeSpan { + // TimeOnly is tick-precise, so the nanosecond count is always a multiple of 100. + return TimeSpan_fromTicks(Math.round(totalNanoseconds(t) / 100)); +} + +// Kept in bigint: a TimeOnly itself always fits a float64 exactly (under 2^47 ns), +// but the amount added does not — a TimeSpan of more than ~104 days already +// exceeds Number.MAX_SAFE_INTEGER in nanoseconds, and .NET wraps it modulo a day +// rather than saturating, so the low ticks are the part that must survive. +function addNanoseconds(t: TimeOnly, deltaNs: bigint, wrappedDays?: FSharpRef): TimeOnly { + const totalNs = BigInt(totalNanoseconds(t)) + deltaNs; + let days = totalNs / nanosecondsPerDayBig; + let remainder = totalNs % nanosecondsPerDayBig; + + // BigInt division truncates toward zero; .NET wraps toward the previous day. + if (remainder < 0n) { + remainder += nanosecondsPerDayBig; + days -= 1n; + } + + if (wrappedDays !== undefined) { + wrappedDays.contents = Number(days); + } + + return fromNanoseconds(Number(remainder)); +} + +export function add(t: TimeOnly, ts: TimeSpan, wrappedDays?: FSharpRef): TimeOnly { + return addNanoseconds(t, TimeSpan_totalNanoseconds(ts), wrappedDays); +} + +export function addHours(t: TimeOnly, h: number): TimeOnly { + // Scale to ticks first, as .NET does, then widen — `h` hours in nanoseconds + // overflows the exact float64 range two orders of magnitude sooner. + return addNanoseconds(t, BigInt(Math.round(h * 36_000_000_000)) * 100n); +} + +export function addMinutes(t: TimeOnly, m: number): TimeOnly { + return addNanoseconds(t, BigInt(Math.round(m * 600_000_000)) * 100n); +} + +export function isBetween(t: TimeOnly, start: TimeOnly, end: TimeOnly): boolean { + return Temporal.PlainTime.compare(start, end) <= 0 + ? (Temporal.PlainTime.compare(start, t) <= 0 && Temporal.PlainTime.compare(end, t) > 0) + : (Temporal.PlainTime.compare(start, t) <= 0 || Temporal.PlainTime.compare(end, t) > 0); +} + +export function equals(x: TimeOnly, y: TimeOnly): boolean { + return x.equals(y); +} + +export function compare(x: TimeOnly, y: TimeOnly): number { + return Temporal.PlainTime.compare(x, y); +} + +export function hash(t: TimeOnly): number { + return totalNanoseconds(t) % 2147483647; +} + +export function op_Subtraction(left: TimeOnly, right: TimeOnly): TimeSpan { + // Returns the elapsed TimeSpan, wrapping around midnight + const ns = (totalNanoseconds(left) - totalNanoseconds(right) + nanosecondsPerDay) % nanosecondsPerDay; + return TimeSpan_fromTicks(Math.round(ns / 100)); +} + +export function toString(t: TimeOnly, format = "t", _provider?: any): string { + switch (format) { + case "t": + return t.toString({ smallestUnit: "minute" }); + case "r": + case "R": + case "T": + return t.toString({ smallestUnit: "second" }); + case "o": + case "O": + // .NET tick precision is 7 fractional digits + return t.toString({ fractionalSecondDigits: 7 }); + default: + throw new Exception("Custom formats are not supported"); + } +} + +export function parse(str: string): TimeOnly { + // Allowed format types: + // hh:mm + // hh:mm:ss + // hh:mm:ss.fffffff + const r = /^\s*([0-1]?\d|2[0-3])\s*:\s*([0-5]?\d)(\s*:\s*([0-5]?\d)(\.(\d+))?)?\s*$/.exec(str); + if (r != null && r[1] != null && r[2] != null) { + const h = +r[1]; + const m = +r[2]; + const s = r[4] != null ? +r[4] : 0; + // .NET resolves the fraction down to a single tick (7 digits) and truncates + // anything finer, so the digits are read as ticks rather than milliseconds. + const subSecondTicks = r[6] != null ? +r[6].padEnd(7, "0").substring(0, 7) : 0; + + return fromNanoseconds(((h * 60 + m) * 60 + s) * 1_000_000_000 + subSecondTicks * 100); + } + + throw new Exception(`String '${str}' was not recognized as a valid TimeOnly.`); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/TimeSpanTemporal.ts b/src/fable-library-ts/TimeSpanTemporal.ts new file mode 100644 index 0000000000..e0011f651a --- /dev/null +++ b/src/fable-library-ts/TimeSpanTemporal.ts @@ -0,0 +1,280 @@ +import { FSharpRef } from "./Types.ts"; +import { toInt64, int64 } from "./BigInt.ts"; +import { Exception, padWithZeros } from "./Util.ts"; + +export type TimeSpan = Temporal.Duration; +export const Duration = Temporal.Duration; +export type Duration = Temporal.Duration; + +// The generic equality/comparison/hashing helpers in Util.ts dispatch at runtime +// on .NET-style Equals/CompareTo/GetHashCode methods. Attach them so TimeSpan +// also works in generic contexts: records, tuples, erased generics, etc. +const proto = Temporal.Duration.prototype as any; +proto.Equals = function (this: TimeSpan, other: TimeSpan): boolean { return Temporal.Duration.compare(this, other) === 0; }; +proto.CompareTo = function (this: TimeSpan, other: TimeSpan): number { return Temporal.Duration.compare(this, other); }; +proto.GetHashCode = function (this: TimeSpan): number { return hash(this); }; +// Lets String.Format format this value without String.ts importing this module (see String.ts) +proto[Symbol.for("Fable.DateTimeFormattable")] = function (this: TimeSpan, format?: string): string { return toString(this, format); }; + +const nsPerMillisecond = 1_000_000n; +const nsPerSecond = 1_000_000_000n; +const nsPerMinute = 60_000_000_000n; +const nsPerHour = 3_600_000_000_000n; +const nsPerDay = 86_400_000_000_000n; + +export function totalNanoseconds(ts: TimeSpan): bigint { + return BigInt(ts.days) * nsPerDay + + BigInt(ts.hours) * nsPerHour + + BigInt(ts.minutes) * nsPerMinute + + BigInt(ts.seconds) * nsPerSecond + + BigInt(ts.milliseconds) * nsPerMillisecond + + BigInt(ts.microseconds) * 1000n + + BigInt(ts.nanoseconds); +} + +function fromNanoseconds(totalNs: bigint): TimeSpan { + const negative = totalNs < 0n; + // A .NET TimeSpan is a whole number of 100ns ticks, so anything finer than a + // tick is dropped (toward zero, as .NET does) rather than carried in the + // Duration's nanosecond field — otherwise values that cannot exist in .NET + // would leak out of Parse/FromMilliseconds/arithmetic. + let n = (negative ? -totalNs : totalNs) / 100n * 100n; + const days = Number(n / nsPerDay); n %= nsPerDay; + const hours = Number(n / nsPerHour); n %= nsPerHour; + const minutes = Number(n / nsPerMinute); n %= nsPerMinute; + const seconds = Number(n / nsPerSecond); n %= nsPerSecond; + const milliseconds = Number(n / nsPerMillisecond); n %= nsPerMillisecond; + const microseconds = Number(n / 1000n); n %= 1000n; + const d = new Temporal.Duration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, Number(n)); + return negative ? d.negated() : d; +} + +// Converts a possibly fractional unit count into an exact nanosecond amount, +// keeping the integer part exact for the full Int64 tick range. +function unitToNanoseconds(value: number, nsPerUnit: bigint): bigint { + const whole = Math.trunc(value); + const frac = value - whole; + return BigInt(whole) * nsPerUnit + BigInt(Math.round(frac * Number(nsPerUnit))); +} + +export function create(d: number = 0, h: number = 0, m: number = 0, s: number = 0, ms: number = 0, us: number = 0): TimeSpan { + switch (arguments.length) { + case 1: + // ticks + return fromTicks(arguments[0]); + case 3: + // h,m,s + d = 0, h = arguments[0], m = arguments[1], s = arguments[2], ms = 0; + break; + default: + // d,h,m,s,ms,us + break; + } + return fromNanoseconds( + unitToNanoseconds(d, nsPerDay) + unitToNanoseconds(h, nsPerHour) + unitToNanoseconds(m, nsPerMinute) + + unitToNanoseconds(s, nsPerSecond) + unitToNanoseconds(ms, nsPerMillisecond) + unitToNanoseconds(us, 1000n)); +} + +export function fromTicks(ticks: number | bigint): TimeSpan { + return fromNanoseconds(BigInt(ticks) * 100n); +} + +export function zero(): TimeSpan { + return new Temporal.Duration(); +} + +const ticksPerUnit = { + days: 864_000_000_000, + hours: 36_000_000_000, + minutes: 600_000_000, + seconds: 10_000_000, + milliseconds: 10_000, +}; + +// The DateTime/DateTimeOffset/TimeOnly Add* members take a double and are +// tick-precise in .NET: the value is scaled to ticks and rounded, so +// AddSeconds(0.0000001) moves exactly one tick and AddDays(1.5) exactly 36 hours. +// Temporal's own `add({ days: 1.5 })` rejects non-integers, so the scaling has to +// happen here. +export function fromUnits(value: number, unit: keyof typeof ticksPerUnit): TimeSpan { + return fromTicks(BigInt(Math.round(value * ticksPerUnit[unit]))); +} + +export function fromDays(d: number, h: number = 0, m: bigint = 0n, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(d, h, Number(m), Number(s), Number(ms)); +} + +export function fromHours(h: number, m: bigint = 0n, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(0, h, Number(m), Number(s), Number(ms)); +} + +export function fromMinutes(m: number | bigint, s: bigint = 0n, ms: bigint = 0n): TimeSpan { + return create(0, 0, Number(m), Number(s), Number(ms)); +} + +export function fromSeconds(s: number | bigint, ms: bigint = 0n): TimeSpan { + return create(0, 0, 0, Number(s), Number(ms)); +} + +export function fromMilliseconds(ms: number | bigint): TimeSpan { + return fromNanoseconds(unitToNanoseconds(Number(ms), nsPerMillisecond)); +} + +export function ticks(ts: TimeSpan): int64 { + return toInt64(totalNanoseconds(ts) / 100n); +} + +// .NET computes each total as (double)Ticks / TicksPerUnit; match that exactly +// (Temporal's own .total() balances differently and can be 1 ULP off). +function totalTicks(ts: TimeSpan): number { + return Number(totalNanoseconds(ts) / 100n); +} + +export function totalDays(ts: TimeSpan): number { + return totalTicks(ts) / 864_000_000_000; +} + +export function totalHours(ts: TimeSpan): number { + return totalTicks(ts) / 36_000_000_000; +} + +export function totalMinutes(ts: TimeSpan): number { + return totalTicks(ts) / 600_000_000; +} + +export function totalSeconds(ts: TimeSpan): number { + return totalTicks(ts) / 10_000_000; +} + +export function totalMilliseconds(ts: TimeSpan): number { + return totalTicks(ts) / 10_000; +} + +export function negate(ts: TimeSpan): TimeSpan { + return ts.negated(); +} + +export function add(ts1: TimeSpan, ts2: TimeSpan): TimeSpan { + return fromNanoseconds(totalNanoseconds(ts1) + totalNanoseconds(ts2)); +} + +export function subtract(ts1: TimeSpan, ts2: TimeSpan): TimeSpan { + return fromNanoseconds(totalNanoseconds(ts1) - totalNanoseconds(ts2)); +} + +// .NET scales the tick count as a double and rounds: `new TimeSpan((long)Math +// .Round(Ticks * factor))`. Scaling ticks rather than nanoseconds keeps the +// magnitude — and so the rounding — identical to .NET's. +export function multiply(ts: TimeSpan, factor: number): TimeSpan { + return fromTicks(BigInt(Math.round(Number(ticks(ts)) * factor))); +} + +export function divide(ts: TimeSpan, factor: number): TimeSpan { + return fromTicks(BigInt(Math.round(Number(ticks(ts)) / factor))); +} + +export function divideByTimeSpan(ts: TimeSpan, other: TimeSpan): number { + return Number(ticks(ts)) / Number(ticks(other)); +} + +export const op_Addition = add; +export const op_Subtraction = subtract; +export const op_Multiply = multiply; +export const op_UnaryNegation = negate; + +export function compare(x: TimeSpan, y: TimeSpan): number { + return Temporal.Duration.compare(x, y); +} + +export const compareTo = compare; + +export function equals(x: TimeSpan, y: TimeSpan): boolean { + return Temporal.Duration.compare(x, y) === 0; +} + +export function hash(ts: TimeSpan): number { + return Number(totalNanoseconds(ts) % 2147483647n); +} + +export function duration(ts: TimeSpan): TimeSpan { + return ts.abs(); +} + +export function toString(ts: TimeSpan, format = "c", _provider?: any): string { + if (["c", "g", "G"].indexOf(format) === -1) { + throw new Exception("Custom formats are not supported"); + } + const d = Math.abs(ts.days); + const h = Math.abs(ts.hours); + const m = Math.abs(ts.minutes); + const s = Math.abs(ts.seconds); + const ticks = Math.abs(ts.milliseconds) * 10_000 + + Math.abs(ts.microseconds) * 10 + + Math.trunc(Math.abs(ts.nanoseconds) / 100); + const sign = ts.sign < 0 ? "-" : ""; + const days = d === 0 && format !== "G" ? "" : format === "c" ? d + "." : d + ":"; + const time = `${format === "g" ? h : padWithZeros(h, 2)}:${padWithZeros(m, 2)}:${padWithZeros(s, 2)}`; + // .NET prints seven tick digits for "c" and "G", trims trailing zeros for "g", and omits a zero fraction for "c" and "g". + const fraction = + format === "g" + ? (ticks === 0 ? "" : "." + padWithZeros(ticks, 7).replace(/0+$/, "")) + : (ticks === 0 && format === "c" ? "" : "." + padWithZeros(ticks, 7)); + return `${sign}${days}${time}${fraction}`; +} + +export function parse(str: string): TimeSpan { + const firstDot = str.search("\\."); + const firstColon = str.search("\\:"); + if (firstDot === -1 && firstColon === -1) { // There is only a day ex: 4 + const d = parseInt(str, 0); + if (isNaN(d)) { + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); + } else { + return create(d, 0, 0, 0, 0); + } + } + if (firstColon > 0) { // process time part + // WIP: (-?)(((\d+)\.)?([0-9]|0[0-9]|1[0-9]|2[0-3]):(\d+)(:\d+(\.\d{1,7})?)?|\d+(?:(?!\.))) + const r = /^(-?)((\d+)\.)?(?:0*)([0-9]|0[0-9]|1[0-9]|2[0-3]):(?:0*)([0-5][0-9]|[0-9])(:(?:0*)([0-5][0-9]|[0-9]))?\.?(\d+)?$/.exec(str); + if (r != null && r[4] != null && r[5] != null) { + let d = 0; + let ms = 0; + let s = 0; + const sign = r[1] != null && r[1] === "-" ? -1 : 1; + const h = +r[4]; + const m = +r[5]; + if (r[3] != null) { + d = +r[3]; + } + if (r[7] != null) { + s = +r[7]; + } + if (r[8] != null) { + // Depending on the number of decimals passed, we need to adapt the numbers + switch (r[8].length) { + case 1: ms = +r[8] * 100; break; + case 2: ms = +r[8] * 10; break; + case 3: ms = +r[8]; break; + case 4: ms = +r[8] / 10; break; + case 5: ms = +r[8] / 100; break; + case 6: ms = +r[8] / 1000; break; + case 7: ms = +r[8] / 10000; break; + default: + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); + } + } + const ts = create(d, h, m, s, ms); + return sign < 0 ? negate(ts) : ts; + } + } + throw new Exception(`String '${str}' was not recognized as a valid TimeSpan.`); +} + +export function tryParse(v: string, defValue: FSharpRef): boolean { + try { + defValue.contents = parse(v); + return true; + } catch { + return false; + } +} diff --git a/src/fable-library-ts/ts/tsconfig.json b/src/fable-library-ts/ts/tsconfig.json index 5ace72b110..8bce17fa58 100644 --- a/src/fable-library-ts/ts/tsconfig.json +++ b/src/fable-library-ts/ts/tsconfig.json @@ -13,7 +13,7 @@ /* Language and Environment */ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2020", "dom", "esnext.temporal"], /* Specify a set of bundled library declaration files that describe the target runtime environment. Temporal types (used by the *Temporal.ts files) live only in lib.esnext.temporal; "dom" is needed for setTimeout/console/TextEncoder since "types": [] excludes @types/node. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ diff --git a/src/fable-library-ts/tsconfig.json b/src/fable-library-ts/tsconfig.json index 5ace72b110..8bce17fa58 100644 --- a/src/fable-library-ts/tsconfig.json +++ b/src/fable-library-ts/tsconfig.json @@ -13,7 +13,7 @@ /* Language and Environment */ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2020", "dom", "esnext.temporal"], /* Specify a set of bundled library declaration files that describe the target runtime environment. Temporal types (used by the *Temporal.ts files) live only in lib.esnext.temporal; "dom" is needed for setTimeout/console/TextEncoder since "types": [] excludes @types/node. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ diff --git a/tests/Js/Main/DateTimeOffsetTests.fs b/tests/Js/Main/DateTimeOffsetTests.fs index 44ca837c55..c52e7557ab 100644 --- a/tests/Js/Main/DateTimeOffsetTests.fs +++ b/tests/Js/Main/DateTimeOffsetTests.fs @@ -613,10 +613,16 @@ let tests = /// NOTE: local utc offset of `usedDate`, NOT `DateTime.Now` /// -> `usedDate` is in september -> summer time in Europe (`+2`)! let localOffset: TimeSpan = +#if FABLE_COMPILER_JAVASCRIPT_TEMPORAL + // usedDate is a Temporal.PlainDateTime (no getTimezoneOffset); the host offset for + // that wall-clock is what a DateTimeOffset built from an Unspecified DateTime uses. + DateTimeOffset(usedDate).Offset +#else #if FABLE_COMPILER !!(usedDate?getTimezoneOffset() * -60_000) #else TimeZoneInfo.Local.GetUtcOffset(usedDate) +#endif #endif let shouldSucceed = Ok () @@ -905,4 +911,34 @@ let tests = let dto: DateTimeOffset = d dto.UtcDateTime |> equal d ] + +// See the note in DateTimeTests.fs: tick precision only holds on .NET and on the +// Temporal representation. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testList "Tick precision" [ + testCase "DateTimeOffset round-trip format prints all seven tick digits" <| fun () -> + let d = DateTimeOffset(638397614451234567L, TimeSpan.Zero) + d.ToString("O", CultureInfo.InvariantCulture) |> equal "2024-01-02T03:04:05.1234567+00:00" + + testCase "DateTimeOffset boundary values have the exact .NET tick counts" <| fun () -> + DateTimeOffset.MinValue.UtcTicks |> equal 0L + DateTimeOffset.MaxValue.UtcTicks |> equal 3155378975999999999L + + testCase "DateTimeOffset keeps sub-millisecond ticks through construction" <| fun () -> + let d = DateTimeOffset(638397614451234567L, TimeSpan.Zero) + d.Ticks % 10_000L |> equal 4567L + d.ToOffset(TimeSpan.FromHours 2.0).UtcTicks |> equal d.UtcTicks + + testCase "DateTimeOffset.Add* accept fractional values and are tick-precise" <| fun () -> + let d = DateTimeOffset(2024, 1, 2, 3, 4, 5, 6, TimeSpan.Zero) + d.AddDays(1.5).Ticks - d.Ticks |> equal 1_296_000_000_000L + d.AddMilliseconds(0.5).Ticks - d.Ticks |> equal 5_000L + + testCase "DateTimeOffset.Now is representable as a whole number of ticks" <| fun () -> + let n = DateTimeOffset.Now + DateTimeOffset(n.Ticks, n.Offset) |> equal n + let u = DateTimeOffset.UtcNow + DateTimeOffset(u.Ticks, u.Offset) |> equal u + ] +#endif ] diff --git a/tests/Js/Main/DateTimeTests.fs b/tests/Js/Main/DateTimeTests.fs index 83ad64acc1..a0d3f71aa0 100644 --- a/tests/Js/Main/DateTimeTests.fs +++ b/tests/Js/Main/DateTimeTests.fs @@ -1274,4 +1274,58 @@ let tests = let local = DateTime(2020, 2, 20, 20, 20, 20, DateTimeKind.Local) let utc = DateTime(2020, 2, 20, 20, 20, 20, DateTimeKind.Utc) sprintf "%A" local |> equal (sprintf "%A" utc) + +// A .NET tick is 100ns, four digits finer than a JS Date can represent, so these +// only hold where the runtime keeps ticks: on .NET, and on the Temporal +// representation. The default JS-Date representation truncates to milliseconds. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "DateTime keeps sub-millisecond ticks through SpecifyKind" <| fun () -> + let d = DateTime(638397614451234567L, DateTimeKind.Utc) + DateTime.SpecifyKind(d, DateTimeKind.Local).Ticks |> equal d.Ticks + // SpecifyKind must not mutate the value it was handed + d.Kind |> equal DateTimeKind.Utc + + testCase "DateTime keeps sub-millisecond ticks through time zone conversion" <| fun () -> + let utc = DateTime(638397614451234567L, DateTimeKind.Utc) + let local = DateTime(638397614451234567L, DateTimeKind.Local) + // The offset is a whole number of minutes, so the sub-second remainder survives + utc.ToLocalTime().Ticks % 10_000L |> equal 4567L + local.ToUniversalTime().Ticks % 10_000L |> equal 4567L + + testCase "DateTime round-trip format prints all seven tick digits" <| fun () -> + let d = DateTime(638397614451234567L, DateTimeKind.Utc) + d.ToString("O", CultureInfo.InvariantCulture) |> equal "2024-01-02T03:04:05.1234567Z" + d.ToString("fffffff", CultureInfo.InvariantCulture) |> equal "1234567" + d.ToString("FFFFFFF", CultureInfo.InvariantCulture) |> equal "1234567" + // "F" drops trailing zeros, "f" pads them + DateTime(2024, 1, 2, 3, 4, 5, 60).ToString("fffff", CultureInfo.InvariantCulture) |> equal "06000" + DateTime(2024, 1, 2, 3, 4, 5, 60).ToString("FFFFF", CultureInfo.InvariantCulture) |> equal "06" + + testCase "DateTime.Parse recovers sub-millisecond ticks" <| fun () -> + DateTime.Parse("2009-06-15T13:45:30.6175425", CultureInfo.InvariantCulture).Ticks % 10_000_000L + |> equal 6175425L + + testCase "DateTime.Add* accept fractional values and are tick-precise" <| fun () -> + let d = DateTime(2024, 1, 2, 3, 4, 5, 6, DateTimeKind.Utc) + let delta (x: DateTime) = x.Ticks - d.Ticks + d.AddDays(1.5) |> delta |> equal 1_296_000_000_000L + d.AddHours(0.5) |> delta |> equal 18_000_000_000L + d.AddMinutes(1.0 / 3.0) |> delta |> equal 200_000_000L + d.AddSeconds(0.0000001) |> delta |> equal 1L + d.AddMilliseconds(0.5) |> delta |> equal 5_000L + d.AddMilliseconds(0.4) |> delta |> equal 4_000L + d.AddMilliseconds(1.5) |> delta |> equal 15_000L + + testCase "DateTime boundary values have the exact .NET tick counts" <| fun () -> + DateTime.MinValue.Ticks |> equal 0L + DateTime.MaxValue.Ticks |> equal 3155378975999999999L + + testCase "DateTime.Now is representable as a whole number of ticks" <| fun () -> + // Temporal clocks are nanosecond-precise; a value carrying sub-tick + // precision would no longer equal its own tick count round-tripped. + let n = DateTime.Now + DateTime(n.Ticks, n.Kind) |> equal n + let u = DateTime.UtcNow + DateTime(u.Ticks, u.Kind) |> equal u +#endif ] diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 8b2f90eb3d..6317a7b9e5 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -682,6 +682,25 @@ let tests = testList "Strings" [ .Replace(",", ".").Replace(" %", "%") |> equal "0.55 54.67% 2014-09-26 00:19" +// String.Format has to recognise the value at runtime. The default JS +// representation passes a Date for DateOnly and a number for TimeOnly and +// TimeSpan, which are indistinguishable from a DateTime and a float, so only +// .NET and the Temporal representation can format these as .NET does. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "String.Format uses .NET formatting for DateOnly, TimeOnly and TimeSpan" <| fun () -> + let d = DateOnly(2024, 1, 2) + let t = TimeOnly(1, 2, 3) + let ts = TimeSpan.FromHours(1.) + String.Format(CultureInfo.InvariantCulture, "{0}", d) |> equal "01/02/2024" + String.Format(CultureInfo.InvariantCulture, "{0:O}", d) |> equal "2024-01-02" + String.Format(CultureInfo.InvariantCulture, "{0}", t) |> equal "01:02" + String.Format(CultureInfo.InvariantCulture, "{0:T}", t) |> equal "01:02:03" + String.Format(CultureInfo.InvariantCulture, "{0}", ts) |> equal "01:00:00" + String.Format(CultureInfo.InvariantCulture, "{0:g}", ts) |> equal "1:00:00" + String.Format(CultureInfo.InvariantCulture, "{0} {1} {2}", d, t, ts) + |> equal "01/02/2024 01:02 01:00:00" +#endif + testCase "Padding works" <| fun () -> "3.14".PadLeft(10) |> equal " 3.14" "3.14".PadRight(10) |> equal "3.14 " diff --git a/tests/Js/Main/TimeOnlyTests.fs b/tests/Js/Main/TimeOnlyTests.fs index 18868da58c..bb097a2324 100644 --- a/tests/Js/Main/TimeOnlyTests.fs +++ b/tests/Js/Main/TimeOnlyTests.fs @@ -194,13 +194,14 @@ let tests = equal (TimeOnly (0, 0, 5)) (TimeOnly.Parse "00:0:5 ") equal TimeOnly.MinValue (TimeOnly.Parse " 0 : 0 : 0 ") equal (TimeOnly (0, 40, 5, 3)) (TimeOnly.Parse "00:40:5.003") - // We're limited to millisecond precision in JS -#if FABLE_COMPILER - equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.0031") - equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.00313213213213") -#else + // The JS-Date representation is limited to millisecond precision; + // the Temporal one is tick-precise, like .NET. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL equal (TimeOnly 50031000L) (TimeOnly.Parse "00:0:5.0031") equal (TimeOnly 50031321L) (TimeOnly.Parse "00:0:5.00313213213213") +#else + equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.0031") + equal (TimeOnly (0, 0, 5, 3)) (TimeOnly.Parse "00:0:5.00313213213213") #endif equal (TimeOnly (0, 0, 59, 30)) (TimeOnly.Parse "00: 0: 59.03") equal (TimeOnly (0, 3, 5, 300)) (TimeOnly.Parse "00 :03:5.3") @@ -268,23 +269,24 @@ let tests = equal true isValid equal (TimeOnly (2, 0, 5, 300)) t - // We're limited to millisecond precision in JS -#if FABLE_COMPILER + // The JS-Date representation is limited to millisecond precision; + // the Temporal one is tick-precise, like .NET. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL let isValid, t = TimeOnly.TryParse "00:0:5.0031" equal true isValid - equal (TimeOnly (0, 0, 5, 3)) t + equal (TimeOnly 50031000L) t let isValid, t = TimeOnly.TryParse "00:0:5.00313213213213" equal true isValid - equal (TimeOnly (0, 0, 5, 3)) t + equal (TimeOnly 50031321L) t #else let isValid, t = TimeOnly.TryParse "00:0:5.0031" equal true isValid - equal (TimeOnly 50031000L) t + equal (TimeOnly (0, 0, 5, 3)) t let isValid, t = TimeOnly.TryParse "00:0:5.00313213213213" equal true isValid - equal (TimeOnly 50031321L) t + equal (TimeOnly (0, 0, 5, 3)) t #endif testCase "Comparison works" <| fun () -> @@ -296,4 +298,29 @@ let tests = let m = [ TimeOnly (20, 5, 1), () ] |> Map.ofList equal true (Map.containsKey (TimeOnly(20, 1, 1).AddMinutes(4.)) m) equal false (Map.containsKey (TimeOnly (20, 1, 1)) m) + +// See the note in DateTimeTests.fs: tick precision only holds on .NET and on the +// Temporal representation. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "TimeOnly.MaxValue is one tick short of a day" <| fun () -> + TimeOnly.MaxValue.Ticks |> equal 863_999_999_999L + + testCase "TimeOnly constructor keeps the microsecond argument" <| fun () -> + TimeOnly(1, 2, 3, 4, 5).Ticks |> equal 37_230_040_050L + + testCase "TimeOnly.Parse keeps all seven fractional digits" <| fun () -> + TimeOnly.Parse("01:02:03.1234567", CultureInfo.InvariantCulture).Ticks % 10_000_000L + |> equal 1234567L + + testCase "TimeOnly.Add wraps a large TimeSpan without losing the low ticks" <| fun () -> + // 100000 whole days plus one tick: the days wrap away and the tick must remain. + // In nanoseconds this amount is past Number.MAX_SAFE_INTEGER. + let big = TimeSpan.FromTicks(864_000_000_000L * 100_000L + 1L) + TimeOnly.MinValue.Add(big).Ticks |> equal 1L + + testCase "TimeOnly.Add* accept fractional values and are tick-precise" <| fun () -> + let t = TimeOnly(1, 2, 3, 4) + t.AddHours(1.0 / 3.0).Ticks - t.Ticks |> equal 12_000_000_000L + t.AddMinutes(0.00001).Ticks - t.Ticks |> equal 6_000L +#endif ] diff --git a/tests/Js/Main/TimeSpanTests.fs b/tests/Js/Main/TimeSpanTests.fs index dd8437b662..eef348bfeb 100644 --- a/tests/Js/Main/TimeSpanTests.fs +++ b/tests/Js/Main/TimeSpanTests.fs @@ -201,7 +201,9 @@ let tests = let t = TimeSpan.FromMilliseconds(ms) let res1 = t.Multiply(factor).TotalMilliseconds let res2 = (t * factor).TotalMilliseconds + let res3 = (factor * t).TotalMilliseconds equal res1 res2 + equal res1 res3 equal true (res1 = res2) equal expected res1 test 1000. -1. -1000. @@ -672,4 +674,47 @@ let tests = let actual = TimeSpan.Parse("-1.23:45:06.78999").Days let expected = -1 equal actual expected + +// A .NET TimeSpan is an integer count of 100ns ticks. The default JS +// representation is a float64 millisecond count, so only .NET and the Temporal +// representation can hold these exactly. +#if !FABLE_COMPILER || FABLE_COMPILER_JAVASCRIPT_TEMPORAL + testCase "TimeSpan holds the full Int64 tick range exactly" <| fun () -> + TimeSpan.FromTicks(1234567890123456789L).Ticks |> equal 1234567890123456789L + TimeSpan.FromTicks(Int64.MaxValue).Ticks |> equal Int64.MaxValue + + testCase "TimeSpan constructor keeps the microsecond argument" <| fun () -> + TimeSpan(0, 0, 0, 0, 0, 5).Ticks |> equal 50L + TimeSpan(1, 2, 3, 4, 5, 6).Ticks |> equal 937_840_050_060L + + testCase "TimeSpan.Parse keeps all seven fractional digits" <| fun () -> + TimeSpan.Parse("00:00:00.1234567").Ticks |> equal 1234567L + + testCase "TimeSpan.ToString keeps all seven fractional digits" <| fun () -> + TimeSpan.FromTicks(1234567L).ToString() |> equal "00:00:00.1234567" + TimeSpan.FromTicks(1L).ToString() |> equal "00:00:00.0000001" + TimeSpan.FromTicks(1234567L).ToString("g", CultureInfo.InvariantCulture) |> equal "0:00:00.1234567" + TimeSpan.FromTicks(1000000L).ToString("g", CultureInfo.InvariantCulture) |> equal "0:00:00.1" + TimeSpan.FromTicks(1234567L).ToString("G", CultureInfo.InvariantCulture) |> equal "0:00:00:00.1234567" + TimeSpan.Parse("-1.02:03:04.0050000").ToString() |> equal "-1.02:03:04.0050000" + + testCase "TimeSpan.FromMilliseconds truncates below a tick" <| fun () -> + // 0.00005ms is half a tick; .NET drops it rather than rounding up + TimeSpan.FromMilliseconds(0.00005).Ticks |> equal 0L + TimeSpan.FromMilliseconds(0.0001).Ticks |> equal 1L + + testCase "TimeSpan multiplication and division are tick-precise" <| fun () -> + let day = TimeSpan.FromDays(1.0) + (day * 1000.0).Ticks |> equal 864_000_000_000_000L + (day / 3.0).Ticks |> equal 288_000_000_000L + day / TimeSpan.FromHours(1.0) |> equal 24.0 + + testCase "TimeSpan arithmetic keeps single ticks" <| fun () -> + let a = TimeSpan.FromTicks(1234567L) + let b = TimeSpan.FromTicks(1L) + (a + b).Ticks |> equal 1234568L + (a - b).Ticks |> equal 1234566L + (-a).Ticks |> equal -1234567L + a.Duration().Ticks |> equal 1234567L +#endif ] diff --git a/tests/TypeScript/tsconfig.json b/tests/TypeScript/tsconfig.json index db3671e62d..8bc46c9334 100644 --- a/tests/TypeScript/tsconfig.json +++ b/tests/TypeScript/tsconfig.json @@ -2,6 +2,9 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "es2020", + // Temporal types (used by the --jsTemporal library files) live in lib.esnext.temporal; + // the rest mirrors the default lib set for target es2020. + "lib": ["es2020", "dom", "esnext.temporal"], "module": "es2020", "rewriteRelativeImportExtensions": true, "erasableSyntaxOnly": true, From 8aa6d3b9e9f3c1dc013d67406878ea93637ad74c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 7 Sep 2026 18:46:30 +0200 Subject: [PATCH 037/322] fix(ts): declare local function type params only reachable via constraints (#4943) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 36 ++++++++++++++--------- tests/Js/Main/TypeTests.fs | 16 ++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index c162811bd1..eb19dd02de 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -1339,24 +1339,32 @@ module Util = // let e = defaultArg eOpt (Expression.numericLiteral (0.)) Expression.unaryExpression ("void", e, ?loc = range) - let getTypeParameters (ctx: Context) (types: Fable.Type list) = - let mutable scopedTypeParams = ctx.ScopedTypeParams + let private getConstraintTypes (c: Fable.Constraint) = + match c with + | Fable.Constraint.CoercesTo target -> [ target ] + | Fable.Constraint.IsDelegate(argsType, retType) -> [ argsType; retType ] + | Fable.Constraint.IsEnum baseType -> [ baseType ] + | Fable.Constraint.SimpleChoice types -> types + | _ -> [] - let typeParams = - types - |> FSharp2Fable.Util.getGenParamTypes - |> List.filter (fun typ -> + let getTypeParameters (ctx: Context) (types: Fable.Type list) = + // A type parameter can appear only in another one's constraint (e.g. `$b :> Lazy<$c>`) and still be referenced by the body. + let rec collect state types = + (state, FSharp2Fable.Util.getGenParamTypes types) + ||> List.fold (fun (scopedTypeParams, typeParams) typ -> match typ with - | Fable.GenericParam(name = name) -> - if Set.contains name scopedTypeParams then - false - else - scopedTypeParams <- Set.add name scopedTypeParams - true - | _ -> false + | Fable.GenericParam(name = name; constraints = constraints) when + not (Set.contains name scopedTypeParams) + -> + constraints + |> List.collect getConstraintTypes + |> collect (Set.add name scopedTypeParams, typ :: typeParams) + | _ -> scopedTypeParams, typeParams ) - scopedTypeParams, typeParams + let scopedTypeParams, typeParams = collect (ctx.ScopedTypeParams, []) types + + scopedTypeParams, List.rev typeParams [] type MemberKind = diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index f74512a4a2..6de95523ff 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -873,6 +873,18 @@ let inline outerGet<'a, 'b when 'a: (static member GetValue: unit -> string) and 'b: (static member GetValue: unit -> string)> () = innerGet<'a>(), innerGet<'b>() +type LazyTree<'a> = LazyNode of 'a * LazyTree<'a> list + +let describeLazy (x: Lazy<'a>) : string = string (x.Force()) + +let walkLazyTree (tree: LazyTree>) : string = + let rec loop (LazyNode(root, xs)) = + match xs with + | [] -> describeLazy root + | x :: _ -> loop x + + loop tree + let tests = testList "Types" [ @@ -1262,6 +1274,10 @@ let tests = search "b" |> equal (Some "b") search "d" |> equal None + testCase "Local function generic over a Lazy subtype constraint works" <| fun () -> + LazyNode(lazy 5, [LazyNode(lazy 7, [])]) |> walkLazyTree |> equal "7" + LazyNode(lazy 5, []) |> walkLazyTree |> equal "5" + testCase "Secondary constructors work" <| fun () -> let s1 = SecondaryCons(3) let s2 = SecondaryCons() From 5af37779ce33c1e9036df7a9548830c4f914bdd5 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 7 Sep 2026 19:18:42 +0200 Subject: [PATCH 038/322] fix(python): build typed arrays from lists and sequences (#4944) --- src/Fable.Transforms/Python/Replacements.fs | 11 ++--------- tests/Js/Main/ConvertTests.fs | 6 ++++++ tests/Python/TestGuid.fs | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 6cd219feac..04d7f63c01 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2062,15 +2062,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex match i.CompiledName, args with | "ToSeq", [ arg ] -> Some arg - | "OfSeq", [ arg ] -> - let meth = Naming.lowerFirst i.CompiledName - let args = injectArg com ctx r "Array" meth i.GenericArgs args - - Helper.LibCall(com, "array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfList", [ arg ] -> - Helper.LibCall(com, "list", "to_array", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ("OfSeq" | "OfList"), [ arg ] -> makeArrayFrom (getElementType t) arg |> Some | "ToList", args -> Helper.LibCall(com, "list", "of_array", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -2156,6 +2148,7 @@ let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Exp Helper.LibCall(com, "seq2", "List_" + meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "ToArray", [ arg ] -> makeArrayFrom (getElementType t) arg |> Some | meth, _ -> let meth = Naming.lowerFirst meth let args = injectArg com ctx r "List" meth i.GenericArgs args diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index b9d62e6c3e..c1c5c77dc3 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -2299,6 +2299,12 @@ let tests = let g = Guid [|6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy|] g.ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + testCase "Convert byte array built from a list or seq to Guid works" <| fun () -> + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(List.toArray bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + testCase "Guid.ToString works with formats" <| fun () -> let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5") let g2 = Guid.Parse(id "96258006-c4ba-4a7f-80c4-de7f2b2898c5") diff --git a/tests/Python/TestGuid.fs b/tests/Python/TestGuid.fs index 04cf7aad89..f58410474b 100644 --- a/tests/Python/TestGuid.fs +++ b/tests/Python/TestGuid.fs @@ -157,6 +157,13 @@ let ``test Convert byte array to Guid works`` () = let g = Guid [|6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy|] g.ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" +[] +let ``test Convert byte array built from a list or seq to Guid works`` () = + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(List.toArray bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + // [] // let ``Guid.ToString works with formats`` () = // let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5") From 93c2df5ba2b3316893dc85dc4fd746a09a07bf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:28:49 -0700 Subject: [PATCH 039/322] fix(rust): Fix IEnumerable casting, interop and type testing (#4946) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 15 +- src/Fable.Transforms/Rust/Replacements.fs | 2 + src/fable-library-rust/src/Interop.rs | 102 +++--- src/fable-library-rust/src/Map.fs | 7 +- src/fable-library-rust/src/Mutable.rs | 11 +- src/fable-library-rust/src/Native.rs | 25 ++ src/fable-library-rust/src/NativeArray.rs | 37 ++ src/fable-library-rust/src/Set.fs | 2 + tests/Rust/tests/src/ArrayTests.fs | 8 + tests/Rust/tests/src/ListTests.fs | 8 + tests/Rust/tests/src/MapTests.fs | 8 + tests/Rust/tests/src/SetTests.fs | 8 + tests/Rust/tests/src/TypeTests.fs | 390 ++++++++++++++++++++-- 13 files changed, 533 insertions(+), 90 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index b830194915..1562d104be 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1632,6 +1632,10 @@ module Util = | Fable.String, IEnumerable _ -> let chars = makeLibCall com ctx None "String" "toCharArray" [ expr ] makeLibCall com ctx None "Seq" "ofArray" [ chars ] + | Replacements.Util.Builtin(Replacements.Util.FSharpSet genArg), IEnumerable _ -> + makeLibCall com ctx None "Set" "toEnumerable" [ expr ] + | Replacements.Util.Builtin(Replacements.Util.FSharpMap(k, v)), IEnumerable _ -> + makeLibCall com ctx None "Map" "toEnumerable" [ expr ] | Replacements.Util.IsEntity (Types.hashset) _, IEnumerable _ | Replacements.Util.IsEntity (Types.iset) _, IEnumerable _ -> let ar = makeLibCall com ctx None "HashSet" "entries" [ expr ] @@ -3950,13 +3954,22 @@ module Util = | _ -> None let simplifyDecisionTree (treeExpr: Fable.Expr) = + let rec containsTypeTest expr = + match expr with + | Fable.Test(_, Fable.TypeTest _, _) -> true + | expr -> getSubExpressions expr |> List.exists containsTypeTest + treeExpr |> visitFromInsideOut ( function | Fable.IfThenElse(guardExpr1, Fable.IfThenElse(guardExpr2, thenExpr, Fable.DecisionTreeSuccess(index2, [], _), _), Fable.DecisionTreeSuccess(index1, [], t), - r) when index1 = index2 -> + r) when + index1 = index2 + && not (containsTypeTest guardExpr1) + && not (containsTypeTest guardExpr2) + -> Fable.IfThenElse( makeLogOp None guardExpr1 guardExpr2 LogicalAnd, thenExpr, diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 1165752f61..cc4dbae578 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -340,6 +340,8 @@ let toSeq com t (expr: Expr) = | IEnumerable -> expr | List _ -> Helper.LibCall(com, "Seq", "ofList", t, [ expr ]) | Array _ -> Helper.LibCall(com, "Seq", "ofArray", t, [ expr ]) + | Builtin(FSharpMap _) -> Helper.LibCall(com, "Map", "toEnumerable", t, [ expr ]) + | Builtin(FSharpSet _) -> Helper.LibCall(com, "Set", "toEnumerable", t, [ expr ]) | String -> let chars = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) Helper.LibCall(com, "Seq", "ofArray", t, [ chars ]) diff --git a/src/fable-library-rust/src/Interop.rs b/src/fable-library-rust/src/Interop.rs index 1d7633dce5..ac21351720 100644 --- a/src/fable-library-rust/src/Interop.rs +++ b/src/fable-library-rust/src/Interop.rs @@ -1,19 +1,25 @@ pub mod ListExt { // use core::ops::Deref; use crate::List_::{List, cons, empty, isEmpty, reverse}; - use crate::Native_::{NullableRef, Vec, seq_to_iter}; + use crate::Native_::{NullableRef, Vec}; use crate::Seq_::ofList; - impl List { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = ofList(self.clone()); - // seq_to_iter(s) - // } + pub struct ListIterator { + list: List, + } - pub fn into_iter(&self) -> impl Iterator { - let s = ofList(self.clone()); - seq_to_iter(s) + impl Iterator for ListIterator { + type Item = T; + + fn next(&mut self) -> Option { + match self.list.root.as_ref() { + Some(node) => { + let head = node.head.clone(); + self.list.root = node.tail.root.clone(); + Some(head) + } + None => None, + } } } @@ -67,6 +73,15 @@ pub mod ListExt { } } + impl IntoIterator for List { + type Item = T; + type IntoIter = ListIterator; + + fn into_iter(self) -> Self::IntoIter { + ListIterator { list: self } + } + } + impl Into> for List { fn into(self) -> Vec { self.into_iter().collect() @@ -75,24 +90,13 @@ pub mod ListExt { } pub mod SetExt { - use crate::Native_::{Func2, Hashable, NullableRef, Vec, combineHashCodes, make_compare, seq_to_iter}; + use crate::Native_::{ + Func2, Hashable, NullableRef, SeqIterator, Vec, combineHashCodes, make_compare, + }; use crate::Set_::{Set, add, compareTo, empty, equals, isEmpty, toSeq}; use core::cmp::Ordering; use core::hash::{Hash, Hasher}; - impl Set { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = toSeq(self.clone()); - // seq_to_iter(s) - // } - - pub fn into_iter(&self) -> impl Iterator { - let s = toSeq(self.clone()); - seq_to_iter(s) - } - } - impl NullableRef for Set { #[inline] fn null() -> Self { @@ -115,9 +119,8 @@ pub mod SetExt { impl Hashable for Set { fn getHashCode(&self) -> i32 { - let s = toSeq(self.clone()); let mut res = 0_i32; - for value in seq_to_iter(s) { + for value in self.clone().into_iter() { res = combineHashCodes(res, value.getHashCode()); } res @@ -126,8 +129,7 @@ pub mod SetExt { impl Hash for Set { fn hash(&self, state: &mut H) { - let s = toSeq(self.clone()); - seq_to_iter(s).for_each(|x| x.hash(state)) + self.clone().into_iter().for_each(|x| x.hash(state)) } } @@ -173,6 +175,15 @@ pub mod SetExt { } } + impl IntoIterator for Set { + type Item = T; + type IntoIter = SeqIterator; + + fn into_iter(self) -> Self::IntoIter { + SeqIterator::new(toSeq(self)) + } + } + impl Into> for Set { fn into(self) -> Vec { self.into_iter().collect() @@ -181,24 +192,13 @@ pub mod SetExt { } pub mod MapExt { - use crate::Map_::{Map, add, compareTo, empty, equals, isEmpty, iterate, toSeq}; - use crate::Native_::{Func2, Hashable, NullableRef, Vec, combineHashCodes, make_compare, seq_to_iter}; + use crate::Map_::{Map, add, compareTo, empty, equals, isEmpty, iterate, toEnumerable}; + use crate::Native_::{ + Func2, Hashable, LrcPtr, NullableRef, SeqIterator, Vec, combineHashCodes, make_compare, + }; use core::cmp::Ordering; use core::hash::{Hash, Hasher}; - impl Map { - //todo - non-consuming iter by ref - // pub fn iter<'a>(&self) -> impl Iterator { - // let s = toSeq(self.clone()); - // seq_to_iter(s).map(|kvp| kvp.as_ref().clone()) - // } - - pub fn into_iter(&self) -> impl Iterator { - let s = toSeq(self.clone()); - seq_to_iter(s).map(|kvp| kvp.as_ref().clone()) - } - } - impl NullableRef for Map { #[inline] fn null() -> Self { @@ -221,10 +221,8 @@ pub mod MapExt { impl Hashable for Map { fn getHashCode(&self) -> i32 { - let s = toSeq(self.clone()); let mut res = 0_i32; - for kvp in seq_to_iter(s) { - let (key, value) = kvp.as_ref(); + for (key, value) in self.clone().into_iter() { res = combineHashCodes(res, key.getHashCode()); res = combineHashCodes(res, value.getHashCode()); } @@ -234,8 +232,7 @@ pub mod MapExt { impl Hash for Map { fn hash(&self, state: &mut H) { - let s = toSeq(self.clone()); - seq_to_iter(s).for_each(|kvp| kvp.hash(state)) + self.clone().into_iter().for_each(|kvp| kvp.hash(state)) } } @@ -271,6 +268,15 @@ pub mod MapExt { } } + impl IntoIterator for Map { + type Item = (K, V); + type IntoIter = SeqIterator; + + fn into_iter(self) -> Self::IntoIter { + SeqIterator::new(toEnumerable(self)) + } + } + impl Into> for Map { fn into(self) -> Vec<(K, V)> { self.into_iter().collect() diff --git a/src/fable-library-rust/src/Map.fs b/src/fable-library-rust/src/Map.fs index e7e09ed839..35cf8659a8 100644 --- a/src/fable-library-rust/src/Map.fs +++ b/src/fable-library-rust/src/Map.fs @@ -561,7 +561,7 @@ let current i = | { root = Some t } :: _ -> if t.Height = 1 then // KeyValuePair<_, _>(t.Key, t.Value) - (t.Key, t.Value) + struct (t.Key, t.Value) else unexpectedStackForCurrent () | _ -> alreadyFinished () @@ -582,7 +582,7 @@ let rec moveNext i = i.started <- true // The first call to MoveNext "starts" the enumeration. not i.stack.IsEmpty -let toSeq (m: Map<'K, 'V>) = +let toEnumerable (m: Map<'K, 'V>) = Seq.delay (fun () -> mkIterator m |> Seq.unfold (fun i -> @@ -593,6 +593,9 @@ let toSeq (m: Map<'K, 'V>) = ) ) +let toSeq (m: Map<'K, 'V>) = + Seq.map (fun struct (k, v) -> (k, v)) (toEnumerable m) + let compareTo (m1: Map<'K, 'V>) (m2: Map<'K, 'V>) = // LanguagePrimitives.GenericComparison m1 m2 Seq.compareWith compare (toSeq m1) (toSeq m2) diff --git a/src/fable-library-rust/src/Mutable.rs b/src/fable-library-rust/src/Mutable.rs index 43d64ec40b..7fcbaf1918 100644 --- a/src/fable-library-rust/src/Mutable.rs +++ b/src/fable-library-rust/src/Mutable.rs @@ -17,7 +17,7 @@ mod NonSyncCell { use core::convert::{AsMut, AsRef}; use core::fmt::{Debug, Display, Formatter, Result}; use core::hash::{Hash, Hasher}; - use core::ops::{Deref, DerefMut, Index}; + use core::ops::{Deref, DerefMut}; #[repr(transparent)] pub struct MutCell { @@ -100,15 +100,6 @@ mod NonSyncCell { } } - impl Index for MutCell> { - type Output = T; - - #[inline] - fn index(&self, idx: i32) -> &Self::Output { - &self.as_ref()[idx as usize] - } - } - impl PartialEq for MutCell { #[inline] fn eq(&self, other: &MutCell) -> bool { diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index 6c2a627fa3..e4dd3c6965 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -68,6 +68,7 @@ pub mod Native_ { use crate::System::Collections::Generic::EqualityComparer_1; use crate::System::Collections::Generic::IEnumerable_1; + use crate::System::Collections::Generic::IEnumerator_1; use crate::System::Collections::Generic::IEqualityComparer_1; // TODO: use these types in generated code @@ -809,6 +810,30 @@ pub mod Native_ { // Sequences // ----------------------------------------------------------- + pub struct SeqIterator { + en: LrcPtr>, + } + + impl SeqIterator { + pub fn new(seq: Seq) -> Self { + SeqIterator { + en: seq.GetEnumerator(), + } + } + } + + impl Iterator for SeqIterator { + type Item = T; + + fn next(&mut self) -> Option { + if self.en.MoveNext() { + Some(self.en.get_Current().clone()) + } else { + None + } + } + } + pub fn seq_to_iter(seq: Seq) -> impl Iterator where T: Clone + 'static, diff --git a/src/fable-library-rust/src/NativeArray.rs b/src/fable-library-rust/src/NativeArray.rs index c82b69b89b..ecc0a7f269 100644 --- a/src/fable-library-rust/src/NativeArray.rs +++ b/src/fable-library-rust/src/NativeArray.rs @@ -14,6 +14,25 @@ pub mod NativeArray_ { pub type Array = LrcPtr>; + pub struct ArrayIterator { + arr: MutArray, + pos: usize, + } + + impl Iterator for ArrayIterator { + type Item = T; + + fn next(&mut self) -> Option { + if self.pos < self.arr.len() { + let item = self.arr[self.pos as i32].clone(); + self.pos += 1; + Some(item) + } else { + None + } + } + } + impl core::ops::Deref for MutArray { type Target = MutCell>; fn deref(&self) -> &Self::Target { @@ -47,6 +66,24 @@ pub mod NativeArray_ { } } + impl core::ops::Index for MutArray { + type Output = T; + + #[inline] + fn index(&self, idx: i32) -> &Self::Output { + &self.0.get()[idx as usize] + } + } + + impl IntoIterator for MutArray { + type Item = T; + type IntoIter = ArrayIterator; + + fn into_iter(self) -> Self::IntoIter { + ArrayIterator { arr: self, pos: 0 } + } + } + impl Into> for MutArray { fn into(self) -> Vec { self.0.get().iter().cloned().collect() diff --git a/src/fable-library-rust/src/Set.fs b/src/fable-library-rust/src/Set.fs index da6fd60563..e7fc3fdbba 100644 --- a/src/fable-library-rust/src/Set.fs +++ b/src/fable-library-rust/src/Set.fs @@ -638,6 +638,8 @@ let toSeq (s: Set<'T>) = ) ) +let toEnumerable (s: Set<'T>) = toSeq s + let compareTo (s1: Set<'T>) (s2: Set<'T>) = // LanguagePrimitives.GenericComparison s1 s2 Seq.compareWith compare (toSeq s1) (toSeq s2) diff --git a/tests/Rust/tests/src/ArrayTests.fs b/tests/Rust/tests/src/ArrayTests.fs index ed8d8830b4..b860351d83 100644 --- a/tests/Rust/tests/src/ArrayTests.fs +++ b/tests/Rust/tests/src/ArrayTests.fs @@ -989,6 +989,14 @@ let ``Array.toSeq works`` () = ys |> Seq.head |> equal 1. +[] +let ``Arrays can be enumerated`` () = + let xs = [|1.; 2.; 3.; 4.|] + let mutable total = 0 + for x in (xs :> seq<_>) do + total <- total + int x + total |> equal 10 + [] let ``Array.truncate works`` () = let xs = [|1.; 2.; 3.; 4.; 5.|] diff --git a/tests/Rust/tests/src/ListTests.fs b/tests/Rust/tests/src/ListTests.fs index 9ad39191ac..456d27e883 100644 --- a/tests/Rust/tests/src/ListTests.fs +++ b/tests/Rust/tests/src/ListTests.fs @@ -659,6 +659,14 @@ let ``List.toSeq works`` () = |> Seq.tail |> Seq.head |> equal 2 +[] +let ``Lists can be enumerated`` () = + let xs = [1.; 2.; 3.; 4.] + let mutable total = 0 + for x in xs do + total <- total + int x + total |> equal 10 + [] let ``List.tryPick works`` () = [1; 2] diff --git a/tests/Rust/tests/src/MapTests.fs b/tests/Rust/tests/src/MapTests.fs index 2e456284ed..00854db440 100644 --- a/tests/Rust/tests/src/MapTests.fs +++ b/tests/Rust/tests/src/MapTests.fs @@ -335,6 +335,14 @@ let ``Map.toSeq works`` () = (Seq.item 2 xs |> snd) = (Seq.item 2 zs |> snd) |> equal true +[] +let ``Maps can be enumerated`` () = + let xs = Map [1,1.; 2,4.; 3,9.; 4,16.] + let mutable total = 0 + for KeyValue(k, v) in xs do + total <- total + k + int v + total |> equal 40 + [] let ``Map.toSeq generates sequences that can be iterated multiple times`` () = // See #2242 let pr (sequence: seq) = diff --git a/tests/Rust/tests/src/SetTests.fs b/tests/Rust/tests/src/SetTests.fs index 3beab8dc32..d5a6e62512 100644 --- a/tests/Rust/tests/src/SetTests.fs +++ b/tests/Rust/tests/src/SetTests.fs @@ -363,6 +363,14 @@ let ``Set.toSeq works`` () = (Seq.item 2 xs) = (Seq.item 2 zs) |> equal true +[] +let ``Sets can be enumerated`` () = + let xs = set [1.; 2.; 3.; 4.] + let mutable total = 0 + for x in xs do + total <- total + int x + total |> equal 10 + [] let ``Comparing large sets works`` () = // See #2203 let largeSetA = Set.ofArray [| for i in 1 .. 100_000 -> i |] diff --git a/tests/Rust/tests/src/TypeTests.fs b/tests/Rust/tests/src/TypeTests.fs index cde8f668e6..f7855de36e 100644 --- a/tests/Rust/tests/src/TypeTests.fs +++ b/tests/Rust/tests/src/TypeTests.fs @@ -275,19 +275,11 @@ type ValueTypeR = type StructUnion = Value of string [] -type SimpleRecord = { A: string; B: string } - -[] -type NestedStruct = - val A: float - val B: bool - new(a, b) = { A = a; B = b } - -[] -type TopLevelStruct = - val A: float - val B: NestedStruct - new(a, b) = { A = a; B = b } +type SimpleRecord = + { A: string; B: string } + member this.WithA(a: string) = + let this = this + { this with A = a } type Point2D = struct @@ -344,6 +336,118 @@ type TypeWithClassAttribute = // [] val mutable StringValue: string // [] val mutable ObjValue: System.Collections.Generic.Dictionary +// [] +// type MyUser = +// class end + +// // See #2739: static [] fields must be zero-initialized in JS/TS +// type ClassWithDefaultValueStaticFields() = + +// [] +// static val mutable private int: int +// [] +// static val mutable private bool: bool +// [] +// static val mutable private string: string +// [] +// static val mutable private guid: Guid +// [] +// static val mutable private char: char + +// [] +// static val mutable private dateTime: DateTime + +// [] +// static val mutable private timeOnly : TimeOnly +// [] +// static val mutable private dateOnly : DateOnly +// [] +// static val mutable private dateTimeOffset : DateTimeOffset +// [] +// static val mutable private int8 : int8 +// [] +// static val mutable private uInt8 : uint8 +// [] +// static val mutable private int16 : Int16 +// [] +// static val mutable private uInt16 : UInt16 +// [] +// static val mutable private int32 : Int32 +// [] +// static val mutable private uInt32 : UInt32 +// [] +// static val mutable private int64 : Int64 +// [] +// static val mutable private uInt64 : UInt64 +// [] +// static val mutable private bigInt : bigint +// [] +// static val mutable private nativeInt : nativeint +// [] +// static val mutable private uNativeInt : unativeint +// [] +// static val mutable private float32 : float32 +// [] +// static val mutable private float64 : float +// [] +// static val mutable private decimal : Decimal + +// [] +// static val mutable private timeSpan: TimeSpan +// [] +// static val mutable private allowNullLiteralClass: MyUser +// [] +// static val mutable private nullableInt: Nullable +// [] +// static val mutable private tuple2: TimeSpan * DateTime +// [] +// static val mutable private structTuple2: struct (TimeSpan * DateTime) + +// static member IncrCount() = +// ClassWithDefaultValueStaticFields.int <- ClassWithDefaultValueStaticFields.int + 1 +// ClassWithDefaultValueStaticFields.int + +// static member Int = ClassWithDefaultValueStaticFields.int +// static member Bool = ClassWithDefaultValueStaticFields.bool +// static member String = ClassWithDefaultValueStaticFields.string +// static member Guid = ClassWithDefaultValueStaticFields.guid +// static member Char = ClassWithDefaultValueStaticFields.char +// static member TimeSpan = ClassWithDefaultValueStaticFields.timeSpan +// static member AllowNullLiteralClass = ClassWithDefaultValueStaticFields.allowNullLiteralClass +// static member NullableInt = ClassWithDefaultValueStaticFields.nullableInt +// static member Tuple2 = ClassWithDefaultValueStaticFields.tuple2 +// static member DateTime = ClassWithDefaultValueStaticFields.dateTime +// static member TimeOnly = ClassWithDefaultValueStaticFields.timeOnly +// static member DateOnly = ClassWithDefaultValueStaticFields.dateOnly +// static member DateTimeOffset = ClassWithDefaultValueStaticFields.dateTimeOffset +// static member Int8 = ClassWithDefaultValueStaticFields.int8 +// static member UInt8 = ClassWithDefaultValueStaticFields.uInt8 +// static member Int16 = ClassWithDefaultValueStaticFields.int16 +// static member UInt16 = ClassWithDefaultValueStaticFields.uInt16 +// static member Int32 = ClassWithDefaultValueStaticFields.int32 +// static member UInt32 = ClassWithDefaultValueStaticFields.uInt32 +// static member Int64 = ClassWithDefaultValueStaticFields.int64 +// static member UInt64 = ClassWithDefaultValueStaticFields.uInt64 +// static member BigInt = ClassWithDefaultValueStaticFields.bigInt +// static member NativeInt = ClassWithDefaultValueStaticFields.nativeInt +// static member UNativeInt = ClassWithDefaultValueStaticFields.uNativeInt +// static member Float32 = ClassWithDefaultValueStaticFields.float32 +// static member Float64 = ClassWithDefaultValueStaticFields.float64 +// static member Decimal = ClassWithDefaultValueStaticFields.decimal +// static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2 + +[] +type NestedStruct = + val A: float + val B: bool + new(a, b) = { A = a; B = b } + +[] +type TopLevelStruct = + val A: float + val B: NestedStruct + new(a, b) = { A = a; B = b } + // type Default1 = int // type Distinct1 = @@ -509,10 +613,36 @@ type MangledAbstractClass5(v) = inherit MangledAbstractClass4(v + 5) override _.MyMethod(x: int) = base.MyMethod(x) + v + 7 +[] +type AbstractClassWithResizeArrayProp() = + abstract Warnings: ResizeArray with get + +type IGenericMethodHelpers<'JsonValue> = + abstract encodeString: string -> 'JsonValue + +type IGenericMethodEncodable = + abstract member Encode<'JsonValue> : helpers: IGenericMethodHelpers<'JsonValue> -> 'JsonValue + +// Class implementation that names the method's type parameter ('json) differently +// from the interface ('JsonValue) +type GenericMethodEncodableClass() = + interface IGenericMethodEncodable with + member _.Encode<'json>(helpers: IGenericMethodHelpers<'json>) = helpers.encodeString "x" + +type IGenericMethodBox<'T> = + abstract Get: unit -> 'T + +// The lambda inside `Get` references the outer generic parameter 'item (via the +// captured `value`); it must not redeclare and shadow it. +let mapGenericMethodBox<'item> (value: 'item) : IGenericMethodBox<'item seq> = + { new IGenericMethodBox<'item seq> with + member _.Get() : 'item seq = Seq.map (fun (x: 'item) -> value) [ value ] + } + type ConcreteClass1() = inherit MangledAbstractClass5(2) -// See #3895 - super call in multi-level generic class hierarchy used wrong mangled name +// See #3895 - super call with generic class hierarchy uses wrong overload hash type IGenericAttach3895<'C> = abstract Attach: 'C -> unit @@ -610,6 +740,62 @@ type Model = unit let update (model: Model) = model, () +// Test types for generic parameter static member resolution +type TestTypeA = + static member GetValue() = "A" + static member Combine(x: int, y: int) = x + y + 100 + +type TestTypeB = + static member GetValue() = "B" + static member Combine(x: int, y: int) = x + y + 200 + +type TestTypeC = + static member GetValue() = "C" + static member Combine(x: int, y: int) = x + y + 300 + +// Inline functions for testing multiple generic parameters with static member constraints +let inline getTwoValues<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue() + +let inline getThreeValues<'a, 'b, 'c when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string) + and 'c: (static member GetValue: unit -> string)> () = + 'a.GetValue(), 'b.GetValue(), 'c.GetValue() + +let inline getValuesAndCombine<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'a: (static member Combine: int * int -> int) + and 'b: (static member GetValue: unit -> string) + and 'b: (static member Combine: int * int -> int)> x y = + let aVal = 'a.GetValue() + let bVal = 'b.GetValue() + let aCombined = 'a.Combine(x, y) + let bCombined = 'b.Combine(x, y) + (aVal, aCombined), (bVal, bCombined) + +let inline getReversed<'x, 'y when 'x: (static member GetValue: unit -> string) + and 'y: (static member GetValue: unit -> string)> () = + 'y.GetValue(), 'x.GetValue() + +let inline innerGet<'t when 't: (static member GetValue: unit -> string)> () = + 't.GetValue() + +let inline outerGet<'a, 'b when 'a: (static member GetValue: unit -> string) + and 'b: (static member GetValue: unit -> string)> () = + innerGet<'a>(), innerGet<'b>() + +// type LazyTree<'a> = LazyNode of 'a * LazyTree<'a> list + +// let describeLazy (x: Lazy<'a>) : string = string (x.Force()) + +// let walkLazyTree (tree: LazyTree>) : string = +// let rec loop (LazyNode(root, xs)) = +// match xs with +// | [] -> describeLazy root +// | x :: _ -> loop x + +// loop tree + [] let ``Unit arguments work`` () = update () |> equal ((), ()) @@ -719,8 +905,9 @@ let ``Can implement interface optional properties with object expression`` () = // StaticClass.DefaultParam2(Nullable()) |> equal 3 // StaticClass.DefaultParam2() |> equal 3 +// // See #3326 // [] -// let ``DefaultParameterValue works with null`` () = // See #3326 +// let ``DefaultParameterValue works with null`` () = // StaticClass.DefaultNullParam() |> isNull |> equal true // StaticClass.DefaultNullParam(5) |> isNull |> equal false @@ -863,8 +1050,9 @@ let ``Type test with BigInt`` () = box 5I |> isBigInt |> equal true box 50 |> isBigInt |> equal false +// See #168 [] -let ``Property names don't clash with built-in JS objects`` () = // See #168 +let ``Property names don't clash with built-in JS objects`` () = let gameState = { Now = 1 Map = "dungeon" @@ -992,6 +1180,11 @@ let ``Lazy constructor works`` () = search "b" |> equal (Some "b") search "d" |> equal None +// [] +// let ``Local function generic over a Lazy subtype constraint works`` () = +// LazyNode(lazy 5, [LazyNode(lazy 7, [])]) |> walkLazyTree |> equal "7" +// LazyNode(lazy 5, []) |> walkLazyTree |> equal "5" + [] let ``Secondary constructors work`` () = let s1 = SecondaryCons(3) @@ -1013,8 +1206,9 @@ let ``Multiple constructors work`` () = equal 9 m2.Value equal 14 m3.Value +// // See #505 // [] -// let ``Abstract methods with default work`` () = // See #505 +// let ``Abstract methods with default work`` () = // let x = ConcreteClass() // x.MethodWithDefault() |> equal "Hello " // x.MustImplement() |> equal "World!!" @@ -1028,8 +1222,9 @@ let ``Abstract properties with getters and setters work`` () = x.MyProp <- 2 equal 7 x.MyProp +// See #505 [] -let ``Interface setters don't conflict`` () = // See #505 +let ``Interface setters don't conflict`` () = let x = XISomeInterface () :> ISomeInterface x.Sender |> equal 0 x.Sender <- 5 @@ -1071,8 +1266,9 @@ let ``Interface setters don't conflict`` () = // See #505 // (foo :> IFoo).Foo() |> equal "BARFOO" // mangleFoo foo |> equal "BARFOO" +// // See #1452 // [] -// let ``Interface casting round-trip`` () = // See #1452 +// let ``Interface casting round-trip`` () = // let d = new DowncastTest(3) :> System.IDisposable // let t = d :?> DowncastTest // t.Value |> equal 3 @@ -1081,32 +1277,37 @@ let ``Interface setters don't conflict`` () = // See #505 // | :? DowncastTest as t2 -> t2.Value // | _ -> 5 +// See #701 [] -let ``Calling default implementation of base members don't cause infinite recursion`` () = // See #701 +let ``Calling default implementation of base members don't cause infinite recursion`` () = let x = ExtendedClass() x.Init() |> equal 7 (x :> BaseClass).Init() |> equal 7 +// See #701 [] -let ``Calling default implementation of base properties don't cause infinite recursion`` () = // See #701 +let ``Calling default implementation of base properties don't cause infinite recursion`` () = let x = ExtendedClass() x.Prop |> equal "base-extension" (x :> BaseClass).Prop |> equal "base-extension" +// See #1464 [] -let ``Calling base members works`` () = // See #1464 +let ``Calling base members works`` () = let bar = ExtendedClass2() bar.B() |> equal 1 +// See #569 [] -let ``Circular dependencies work`` () = // See #569 +let ``Circular dependencies work`` () = let location = { name="NY"; employees=[] } let alice = { name="Alice"; age=20.0; location=location } location.name |> equal "NY" alice.age |> equal 20. +// See #568 [] -let ``Value Type records work`` () = // See #568 +let ``Value Type records work`` () = let foo1 = ValueType<_>("foo") let foo2 = ValueType<_>("foo") foo1.Value |> equal "foo" @@ -1164,8 +1365,9 @@ let ``Unchecked.defaultof works for fields on nested structs`` () = top.B.A |> equal 0 top.B.B |> equal false +// See #3371 [] -let ``copying struct records works`` () = // See #3371 +let ``copying struct records works`` () = let simple : SimpleRecord = { A = ""; B = "B" } let simpleRecord = { simple with A = "A" } simpleRecord.A |> equal "A" @@ -1173,6 +1375,14 @@ let ``copying struct records works`` () = // See #3371 simple.A |> equal "" simple.B |> equal "B" +// See #3828 +[] +let ``copy-update expression on this in struct member works`` () = + let r : SimpleRecord = { A = "hello"; B = "world" } + let r2 = r.WithA("foo") + r2.A |> equal "foo" + r2.B |> equal "world" + // [] // let ``Custom F# exceptions work`` () = // try @@ -1205,8 +1415,9 @@ let ``copying struct records works`` () = // See #3371 // with ex -> ex.Message // |> equal "Will I be reraised?" +// // See #1444 // [] -// let ``This context is not lost in closures within implicit constructor`` () = // See #1444 +// let ``This context is not lost in closures within implicit constructor`` () = // ThisContextInConstructor(7).Value() |> equal 7 // [] @@ -1218,8 +1429,9 @@ let ``copying struct records works`` () = // See #3371 // f1.Add2(4, 5) |> equal -1 // f2.Add2(4, 5) |> equal -1 +// See #573 [] -let ``ClassAttribute works`` () = // See #573 +let ``ClassAttribute works`` () = let t1 = TypeWithClassAttribute(8) t1.Pos |> equal 8 @@ -1242,8 +1454,9 @@ let ``ClassAttribute works`` () = // See #573 // // withDefaultValue.ObjValue |> equal Unchecked.defaultof> // // withDefaultValue.ObjValue |> equal null +// // See #2070 // [] -// let ``Private fields don't conflict with parent classes`` () = // See #2070 +// let ``Private fields don't conflict with parent classes`` () = // let a1 = InfoBClass({ InfoA = { Foo = "foo" }; Bar = "bar" }) :> InfoAClass // let a2 = a1.WithFoo("foo2") // a1.Foo |> equal "foo" @@ -1386,8 +1599,9 @@ let ``Non-mangled interfaces work with classes`` () = // typeof.IsInstanceOfType("hello") |> equal false // #if FABLE_COMPILER +// // See #2485 // [] -// let ``Choice with arity 3+ is represented correctly`` () = // See #2485 +// let ``Choice with arity 3+ is represented correctly`` () = // Choice2Of3 55 |> Fable.Core.Reflection.getCaseName |> equal "Choice2Of3" // Choice3Of3 55 |> Fable.Core.Reflection.getCaseName |> equal "Choice3Of3" // #endif @@ -1406,6 +1620,124 @@ let ``Super call works correctly in multi-level generic class hierarchy`` () = // Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf List.ofSeq log |> equal [ "Base"; "Mid"; "Leaf" ] +// Test for generic type parameter static member resolution in inline functions +// https://github.com/fable-compiler/Fable/issues/4093 +[] +let ``Inline function with two generic parameters resolves static members correctly`` () = + let result = getTwoValues() + result |> equal ("A", "B") + +[] +let ``Inline function with three generic parameters resolves static members correctly`` () = + let result = getThreeValues() + result |> equal ("A", "B", "C") + +[] +let ``Inline function with multiple constraints per type parameter works`` () = + let result = getValuesAndCombine 10 20 + result |> equal (("A", 130), ("B", 230)) + +[] +let ``Inline function with reversed type parameter order works`` () = + let result = getReversed() + result |> equal ("B", "A") + +[] +let ``Nested inline functions resolve generic parameters correctly`` () = + let result = outerGet() + result |> equal ("A", "B") + +[] +let ``Different type parameter combinations work correctly`` () = + let result1 = getTwoValues() + result1 |> equal ("B", "A") + + let result2 = getTwoValues() + result2 |> equal ("C", "A") + + let result3 = getTwoValues() + result3 |> equal ("B", "C") + +// // See https://github.com/fable-compiler/Fable/issues/2739 +// [] +// let ``Static [] fields are zero-initialized`` () = +// ClassWithDefaultValueStaticFields.Int |> equal 0 +// ClassWithDefaultValueStaticFields.Bool |> equal false +// ClassWithDefaultValueStaticFields.String |> equal null +// ClassWithDefaultValueStaticFields.Guid |> equal Guid.Empty +// ClassWithDefaultValueStaticFields.Char |> equal '\000' +// ClassWithDefaultValueStaticFields.TimeSpan |> equal TimeSpan.Zero +// ClassWithDefaultValueStaticFields.TimeOnly |> equal TimeOnly.MinValue +// ClassWithDefaultValueStaticFields.DateOnly |> equal DateOnly.MinValue +// ClassWithDefaultValueStaticFields.DateTime |> equal DateTime.MinValue +// ClassWithDefaultValueStaticFields.DateTimeOffset |> equal DateTimeOffset.MinValue +// ClassWithDefaultValueStaticFields.Int8 |> equal 0y +// ClassWithDefaultValueStaticFields.UInt8 |> equal 0uy +// ClassWithDefaultValueStaticFields.Int16 |> equal 0s +// ClassWithDefaultValueStaticFields.UInt16 |> equal 0us +// ClassWithDefaultValueStaticFields.Int32 |> equal 0 +// ClassWithDefaultValueStaticFields.UInt32 |> equal 0u +// ClassWithDefaultValueStaticFields.Int64 |> equal 0L +// ClassWithDefaultValueStaticFields.UInt64 |> equal 0UL +// ClassWithDefaultValueStaticFields.BigInt |> equal 0I +// ClassWithDefaultValueStaticFields.NativeInt |> equal 0n +// ClassWithDefaultValueStaticFields.UNativeInt |> equal 0un +// ClassWithDefaultValueStaticFields.Float32 |> equal 0.f +// ClassWithDefaultValueStaticFields.Float64 |> equal 0. +// ClassWithDefaultValueStaticFields.Decimal |> equal 0M + +// ClassWithDefaultValueStaticFields.IncrCount() |> equal 1 + +[] +let ``Unchecked.defaultof works for fields on structs`` () = + let top = TopLevelStruct() + top.A |> equal 0 + top.B.A |> equal 0 + top.B.B |> equal false + +// [] +// let ``Abstract class property backed by captured variable in object expression works`` () = +// let warnings = ResizeArray() + +// let reader = +// { new AbstractClassWithResizeArrayProp() with +// member __.Warnings = warnings +// } + +// reader.Warnings.Add("Warning 1") +// reader.Warnings.Add("Warning 2") + +// reader.Warnings.Count |> equal 2 + +// [] +// let ``Object expression implementing a generic interface method works`` () = +// let helpers = +// { new IGenericMethodHelpers with +// member _.encodeString v = "S:" + v +// } + +// let encodable = +// { new IGenericMethodEncodable with +// member _.Encode(helpers) = helpers.encodeString "x" +// } + +// encodable.Encode(helpers) |> equal "S:x" + +// [] +// let ``Class implementing a generic interface method with a renamed type parameter works`` () = +// let helpers = +// { new IGenericMethodHelpers with +// member _.encodeString v = "S:" + v +// } + +// let encodable = GenericMethodEncodableClass() :> IGenericMethodEncodable +// encodable.Encode(helpers) |> equal "S:x" + +[] +let ``Nested lambda does not shadow an outer generic type parameter`` () = + let box = mapGenericMethodBox 42 + box.Get() |> List.ofSeq |> equal [ 42 ] + [] let ``ArgumentException with message and inner exception works`` () = let inner = exn "the inner cause" From 2fb46a269bc4827494cab878f6e3f0e1b4052035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:04:17 -0700 Subject: [PATCH 040/322] fix(rust): Restore missing bindings (#4947) --- .vscode/launch.json | 18 ++++++++++++++++++ src/Fable.Transforms/FableTransforms.fs | 1 + src/Fable.Transforms/Rust/Fable2Rust.fs | 13 +------------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ab2609f8b..45401b5196 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -253,6 +253,24 @@ "--noParallelTypeCheck" ] }, + { + "name": "Fable.Cli on ../quicktest-rust", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/src/Fable.Cli/bin/Debug/net10.0/fable.dll", + "args": [ + "--outDir", "${workspaceRoot}/src/quicktest-rust", + "--fableLib", "${workspaceRoot}/temp/fable-library-rust", + "--exclude", "Fable.Core", + "--lang", "Rust", + "--noCache", + "--noParallelTypeCheck" + ], + "cwd": "${workspaceRoot}/src/quicktest-rust", + "stopAtEntry": false, + "console": "internalConsole" + }, { "name": "Fable.Cli on ../fable-test", "type": "coreclr", diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index f54ebf1564..cdd4b4f196 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -466,6 +466,7 @@ module private Transforms = && isErasingCandidate ident && countReferencesUntil 1 ident.Name letBody = 0 && canHaveSideEffects com value + && not (com.Options.Language = Rust) // Rust emits warnings on missing bindings -> // The binding is never read but its value may have side effects (e.g. residue from // inlining CE builder methods like `Combine`/`Run` that discard their argument). diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 1562d104be..1aab73ba9f 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3235,19 +3235,8 @@ module Util = let expr = transformLeaveContext com ctx None e mkExprStmt expr - // Only the block's true tail can omit the trailing `;`. A block-like expression (`if`, - // `match`, nested `{ }`) in a non-tail statement needs it forced via `Semi`, or a - // non-`()`-typed value there is a Rust type error, not just a style choice. let transformExprsAsStmts (com: IRustCompiler) ctx (exprs: Fable.Expr list) : Rust.Stmt list = - match List.rev exprs with - | [] -> [] - | last :: revRest -> - let nonTailStmts = - revRest - |> List.rev - |> List.map (fun e -> com.TransformExpr(ctx, e) |> mkSemiStmt) - - nonTailStmts @ [ transformAsStmt com ctx last ] + List.map (transformAsStmt com ctx) exprs // flatten nested Let binding expressions let rec flattenLet acc (expr: Fable.Expr) = From 19a6522ea517db53dabd5e221206849d382e1025 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 8 Sep 2026 07:47:30 +0200 Subject: [PATCH 041/322] fix(ts/python): erase byref from struct copy-and-update locals (#4945) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 13 ++------- .../Python/Fable2Python.Transforms.fs | 7 +++-- src/Fable.Transforms/Replacements.Util.fs | 10 +++++++ tests/Js/Main/RecordTypeTests.fs | 25 +++++++++++++++++ tests/Python/TestRecordType.fs | 28 ++++++++++++++++++- 5 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index eb19dd02de..6dc75f53db 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -1292,7 +1292,8 @@ module Util = variables |> Seq.distinctBy (fun (id, _) -> id.Name) |> Seq.map (fun (id, value) -> - let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx id.Type value + let idType = Replacements.Util.getIdentValueType com id id.Type + let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx idType value let value = if com.IsTypeScript && Option.isNone value then @@ -2691,15 +2692,7 @@ but thanks to the optimisation done below we get transformBindingExprBody com ctx var value |> assign var.Range (identAsExpr var) let transformBindingAsStatements (com: IBabelCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = - // Compiler-generated copy-update locals (inputRecord, copyOfStruct) are treated as plain - // values at runtime even though their Fable type is byref. Strip the wrapper for TS annotation. - let varType = - if var.IsCompilerGenerated then - match var.Type with - | Replacements.Util.IsByRefType com innerType -> innerType - | typ -> typ - else - var.Type + let varType = Replacements.Util.getIdentValueType com var var.Type if isJsStatement ctx false value then let ta, tp = makeTypeAnnotationWithParametersIfTypeScript com ctx varType None diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 3ae486e5ee..9685c83fbf 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1633,7 +1633,7 @@ let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) ki // Use effective type for field naming (considers type refinement) let narrowedType = match fableExpr with - | Fable.IdentExpr ident -> getNarrowedType ctx ident + | Fable.IdentExpr ident -> getNarrowedType ctx ident |> Replacements.Util.getIdentValueType com ident | _ -> fableExpr.Type let fieldName = Util.applyFieldNaming com narrowedType i.Name true @@ -1757,6 +1757,7 @@ let transformBindingAsExpr (com: IPythonCompiler) ctx (var: Fable.Ident) (value: expr |> assign None (identAsExpr com ctx var), stmts let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) (value: Fable.Expr) = + let varType = Replacements.Util.getIdentValueType com var var.Type let shouldTreatAsStatement = isPyStatement ctx false value let needsErase = needsOptionEraseForBinding value var.Type // Skip type annotation to avoid Option[T] vs T | None mismatch issues with Pyright: @@ -1776,7 +1777,7 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let body = com.TransformAsStatements(ctx, Some(Assign varExpr), value) body else - let ta, stmts = Annotation.typeAnnotation com ctx None var.Type + let ta, stmts = Annotation.typeAnnotation com ctx None varType let decl = Statement.assign (varName, ta) let body = com.TransformAsStatements(ctx, Some(Assign varExpr), value) stmts @ [ decl ] @ body @@ -1789,7 +1790,7 @@ let transformBindingAsStatements (com: IPythonCompiler) ctx (var: Fable.Ident) ( let decl = varDeclaration ctx varName None expr stmts @ decl else - let ta, stmts' = Annotation.typeAnnotation com ctx None var.Type + let ta, stmts' = Annotation.typeAnnotation com ctx None varType // Erase Option wrapper if needed (zero runtime overhead, just for type checker) let expr' = if needsErase then diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 17dbb09d2d..f97e14f0b5 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -981,6 +981,16 @@ let (|IsByRefType|_|) (com: Compiler) = | _ -> ValueNone | _ -> ValueNone +/// F# types the locals it generates for copy-and-update expressions (`inputRecord`, `copyOfStruct`) +/// as byrefs, but Fable binds the struct value itself to them. +let getIdentValueType (com: Compiler) (ident: Ident) (typ: Type) = + if ident.IsCompilerGenerated then + match typ with + | IsByRefType com innerType -> innerType + | typ -> typ + else + typ + [] let (|IsInRefType|_|) (com: Compiler) = function diff --git a/tests/Js/Main/RecordTypeTests.fs b/tests/Js/Main/RecordTypeTests.fs index 13d52494f2..daabe90152 100644 --- a/tests/Js/Main/RecordTypeTests.fs +++ b/tests/Js/Main/RecordTypeTests.fs @@ -56,6 +56,20 @@ type RecordWithStaticMemberVal = { xs: ResizeArray } static member val empty = { xs = ResizeArray() } +[] +type StructRecord = { Size: int; Seed: int; Path: int list } + +type StructRecordHolder = { Tag: int; Data: StructRecord } + +let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path } + +let holdStructRecord (data: StructRecord) (path: int list) = + Some + { + Tag = 1 + Data = { data with Path = List.rev path } + } + let tests = testList "RecordTypes" [ @@ -112,6 +126,17 @@ let tests = record.B |> equal 11 record.C |> equal 6 + testCase "Copy-and-update of a struct record argument works" <| fun () -> + let data: StructRecord = { Size = 1; Seed = 2; Path = [ 3; 4 ] } + let updated = updateStructRecord data [ 5; 6 ] + equal 1 updated.Size + equal 2 updated.Seed + equal [ 6; 5 ] updated.Path + let held = holdStructRecord data [ 7; 8 ] |> Option.get + equal 1 held.Data.Size + equal 2 held.Data.Seed + equal [ 8; 7 ] held.Data.Path + testCase "Recursive record does not cause issues" <| fun () -> let r = { things = [ { things = [] } ] } equal r.things.Length 1 diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index df1c867157..0beec0b55f 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -56,6 +56,20 @@ type RecordWithStaticMemberVal = type RecordA = { OptionalField : string option } +[] +type StructRecord = { Size: int; Seed: int; Path: int list } + +type StructRecordHolder = { Tag: int; Data: StructRecord } + +let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path } + +let holdStructRecord (data: StructRecord) (path: int list) = + Some + { + Tag = 1 + Data = { data with Path = List.rev path } + } + [] let ``test Anonymous records work`` () = let r = makeAnonRec() @@ -226,4 +240,16 @@ let ``test Record with static member val does not share state across instances`` let b = RecordWithStaticMemberVal.empty a.xs.Add 1 equal 1 a.xs.Count - equal 1 b.xs.Count \ No newline at end of file + equal 1 b.xs.Count + +[] +let ``test Copy-and-update of a struct record argument works`` () = + let data: StructRecord = { Size = 1; Seed = 2; Path = [ 3; 4 ] } + let updated = updateStructRecord data [ 5; 6 ] + equal 1 updated.Size + equal 2 updated.Seed + equal [ 6; 5 ] updated.Path + let held = holdStructRecord data [ 7; 8 ] |> Option.get + equal 1 held.Data.Size + equal 2 held.Data.Seed + equal [ 8; 7 ] held.Data.Path From 6fb03090ce328b8c5d6a36422f120dd641aefe3a Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Tue, 8 Sep 2026 08:19:44 +0200 Subject: [PATCH 042/322] fix(js/ts): match .NET for G, E and custom numeric formats --- src/fable-library-ts/Numeric.ts | 13 ++++++++- src/fable-library-ts/String.ts | 31 +++++++++++++++++---- tests/Js/Main/ConvertTests.fs | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/fable-library-ts/Numeric.ts b/src/fable-library-ts/Numeric.ts index 33bb460a72..d4a9c8f192 100644 --- a/src/fable-library-ts/Numeric.ts +++ b/src/fable-library-ts/Numeric.ts @@ -43,6 +43,17 @@ export function multiply(x: Numeric, y: number) { } } +export function divide(x: Numeric, y: number) { + if (typeof x === "number") { + return x / y; + } else if (typeof x === "bigint") { + // Keep the fractional part so the caller can round it like .NET does + return Number(x) / y; + } else { + return x[symbol]().multiply(1 / y); + } +} + export function toFixed(x: Numeric, dp?: number) { if (typeof x === "number") { return x.toFixed(dp); @@ -67,7 +78,7 @@ export function toExponential(x: Numeric, dp?: number) { if (typeof x === "number") { return x.toExponential(dp); } else if (typeof x === "bigint") { - return x; + return Number(x).toExponential(dp); } else { return x[symbol]().toExponential(dp); } diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 193d2d9138..44ad183ec9 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -1,5 +1,5 @@ import { toString as dateToString } from "./Date.ts"; -import { compare as numericCompare, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision } from "./Numeric.ts"; +import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision } from "./Numeric.ts"; import { escape } from "./RegExp.ts"; import { toString } from "./Types.ts"; import { Exception } from "./Util.ts"; @@ -421,9 +421,14 @@ export function format(str: string | object, ...args: any[]) { } } break; - case "e": case "E": - rep = precision != null ? toExponential(rep, precision) : toExponential(rep); + case "e": case "E": { + precision = precision != null ? precision : 6; + rep = String(toExponential(rep, precision)); + // .NET always signs the exponent and pads it to at least three digits + const eIdx = rep.indexOf("e"); + rep = rep.slice(0, eIdx) + format + rep[eIdx + 1] + padLeft(rep.slice(eIdx + 2), 3, "0"); break; + } case "f": case "F": precision = precision != null ? precision : 2; rep = toFixed(rep, precision); @@ -444,7 +449,7 @@ export function format(str: string | object, ...args: any[]) { const paddedExpDigits = expDigits.length < 2 ? "0" + expDigits : expDigits; const eChar = format === "G" ? "E" : "e"; rep = mantissa + eChar + expSign + paddedExpDigits; - } else { + } else if (rep.indexOf(".") >= 0) { rep = trimEnd(trimEnd(rep, "0"), "."); } break; @@ -490,7 +495,23 @@ export function format(str: string | object, ...args: any[]) { if (pattern) { let sign = ""; - rep = (pattern as string).replace(/([0#,]+)(\.[0#]+)?/, (_, intPart: string, decimalPart: string) => { + const patternStr = pattern as string; + + // Each `%` scales the value by 100 and is kept as a literal in the output + const percents = (patternStr.match(/%/g) ?? []).length; + if (percents > 0) { + rep = multiply(rep, Math.pow(100, percents)); + } + + // .NET ignores commas placed after the decimal placeholders, hence the trailing group + rep = patternStr.replace(/([0#,]+)(\.[0#]+)?(,*)/, (_, intPart: string, decimalPart: string) => { + // Commas between the last integer placeholder and the decimal point scale the + // value down by 1000 each; commas anywhere else only turn on digit grouping + const scaleCommas = /,*$/.exec(intPart)![0].length; + if (scaleCommas > 0) { + intPart = intPart.substring(0, intPart.length - scaleCommas); + rep = divide(rep, Math.pow(1000, scaleCommas)); + } if (isLessThan(rep, 0)) { rep = multiply(rep, -1); sign = "-"; diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index c1c5c77dc3..f943c6d93c 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -914,6 +914,54 @@ let tests = // |> adaptExponentValue // |> equal "-255.2357" + testCase "ToString 'G' keeps the trailing zeros of the integer part" + <| fun () -> + (100000).ToString("G", CultureInfo.InvariantCulture) |> equal "100000" + (100).ToString("G", CultureInfo.InvariantCulture) |> equal "100" + (0).ToString("G", CultureInfo.InvariantCulture) |> equal "0" + (-1200).ToString("G", CultureInfo.InvariantCulture) |> equal "-1200" + (100.0).ToString("G", CultureInfo.InvariantCulture) |> equal "100" + (100000L).ToString("G", CultureInfo.InvariantCulture) |> equal "100000" + (100000).ToString("G4", CultureInfo.InvariantCulture) |> equal "1E+05" + (100000).ToString("g4", CultureInfo.InvariantCulture) |> equal "1e+05" + (1.5).ToString("G4", CultureInfo.InvariantCulture) |> equal "1.5" + + testCase "ToString 'E' pads the exponent and keeps its case" + <| fun () -> + (100000).ToString("E", CultureInfo.InvariantCulture) |> equal "1.000000E+005" + (100000).ToString("e", CultureInfo.InvariantCulture) |> equal "1.000000e+005" + (100000).ToString("E0", CultureInfo.InvariantCulture) |> equal "1E+005" + (100000).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.00E+005" + (1234.5678).ToString("E", CultureInfo.InvariantCulture) |> equal "1.234568E+003" + (0.00012).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.20E-004" + (-1234.5).ToString("E2", CultureInfo.InvariantCulture) |> equal "-1.23E+003" + $"{100000L:E}" |> equal "1.000000E+005" + $"{100000L:E2}" |> equal "1.00E+005" + + testCase "ToString with a custom percent format multiplies by 100" + <| fun () -> + (100000).ToString("0.0%", CultureInfo.InvariantCulture) |> equal "10000000.0%" + (0.1234).ToString("0.00%", CultureInfo.InvariantCulture) |> equal "12.34%" + (0.5).ToString("#.##%", CultureInfo.InvariantCulture) |> equal "50%" + (0.42).ToString("0%", CultureInfo.InvariantCulture) |> equal "42%" + (-0.5).ToString("0.0%", CultureInfo.InvariantCulture) |> equal "-50.0%" + (0.5).ToString("0.0%%", CultureInfo.InvariantCulture) |> equal "5000.0%%" + (0.5).ToString("%0.0", CultureInfo.InvariantCulture) |> equal "%50.0" + + testCase "ToString with a custom format scales by trailing commas" + <| fun () -> + (100000).ToString("0,", CultureInfo.InvariantCulture) |> equal "100" + (100000).ToString("0,,", CultureInfo.InvariantCulture) |> equal "0" + (-123456789).ToString("0,,", CultureInfo.InvariantCulture) |> equal "-123" + (1234567890).ToString("#,#,,", CultureInfo.InvariantCulture) |> equal "1,235" + (123456789).ToString("0,,.0", CultureInfo.InvariantCulture) |> equal "123.5" + (1234567.0).ToString("0,,", CultureInfo.InvariantCulture) |> equal "1" + // A comma that is not next to the decimal point only turns on digit grouping + (123456789).ToString("#,,#", CultureInfo.InvariantCulture) |> equal "123,456,789" + (100000).ToString("#,#", CultureInfo.InvariantCulture) |> equal "100,000" + // .NET ignores commas placed after the decimal placeholders + (123456789).ToString("0.0,,", CultureInfo.InvariantCulture) |> equal "123456789.0" + testCase "byte.ToString 'N' works" <| fun () -> (255uy).ToString("N2") |> equal "255.00" From c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Tue, 8 Sep 2026 08:51:56 +0200 Subject: [PATCH 043/322] fix(js/ts): allow the E format specifier on Int64 and UInt64 --- src/Fable.Transforms/Replacements.fs | 2 + src/fable-library-ts/Numeric.ts | 27 +++++++- tests/Js/Main/ConvertTests.fs | 96 +++++++++++++++------------- 3 files changed, 81 insertions(+), 44 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index eec49d7335..d6a984ba28 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2650,6 +2650,8 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA | _ -> "does not support decimal format specifier" |> Some | "e" -> match numberKind with + | Int64 + | UInt64 -> None | BigIntegers _ -> "does not support exponential format specifier" |> Some | _ -> None | "f" diff --git a/src/fable-library-ts/Numeric.ts b/src/fable-library-ts/Numeric.ts index d4a9c8f192..50cddd6bf0 100644 --- a/src/fable-library-ts/Numeric.ts +++ b/src/fable-library-ts/Numeric.ts @@ -74,11 +74,36 @@ export function toPrecision(x: Numeric, sd?: number) { } } +// A bigint can hold more significant digits than a double, so the mantissa is rounded on the +// decimal digits themselves instead of going through Number +function bigintToExponential(x: bigint, dp?: number) { + const sign = x < 0n ? "-" : ""; + let digits = (x < 0n ? -x : x).toString(); + if (x === 0n) { + return sign + (dp ? "0." + "0".repeat(dp) : "0") + "e+0"; + } + dp = dp ?? digits.length - 1; + let exponent = digits.length - 1; + if (digits.length > dp + 1) { + // .NET rounds the discarded digits away from zero + const roundUp = digits.charCodeAt(dp + 1) >= 53; + digits = (BigInt(digits.slice(0, dp + 1)) + (roundUp ? 1n : 0n)).toString(); + if (digits.length > dp + 1) { + digits = digits.slice(0, dp + 1); + exponent += 1; + } + } else { + digits = digits.padEnd(dp + 1, "0"); + } + const mantissa = dp > 0 ? digits[0] + "." + digits.slice(1) : digits[0]; + return sign + mantissa + "e+" + exponent; +} + export function toExponential(x: Numeric, dp?: number) { if (typeof x === "number") { return x.toExponential(dp); } else if (typeof x === "bigint") { - return Number(x).toExponential(dp); + return bigintToExponential(x, dp); } else { return x[symbol]().toExponential(dp); } diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index f943c6d93c..c1a23027bc 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -605,49 +605,49 @@ let tests = // |> adaptExponentValue // |> equal "2.550000000000000e+2" - // testCase "int64.ToString 'E' works" - // <| fun () -> - // (255L).ToString("E2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" - - // (255L).ToString("E15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" - - // (-255L).ToString("E", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-2.550000e+2" - - // (255L).ToString("e2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" - - // (255L).ToString("e15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" - - // (-255L).ToString("e", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-2.550000e+2" - - // testCase "uint64.ToString 'E' works" - // <| fun () -> - // (255UL).ToString("E2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" - - // (255UL).ToString("E15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" - - // (255UL).ToString("e2", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.55e+2" - - // (255UL).ToString("e15", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "2.550000000000000e+2" + testCase "int64.ToString 'E' works" + <| fun () -> + (255L).ToString("E2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" + + (255L).ToString("E15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" + + (-255L).ToString("E", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-2.550000e+2" + + (255L).ToString("e2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" + + (255L).ToString("e15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" + + (-255L).ToString("e", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-2.550000e+2" + + testCase "uint64.ToString 'E' works" + <| fun () -> + (255UL).ToString("E2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" + + (255UL).ToString("E15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" + + (255UL).ToString("e2", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.55e+2" + + (255UL).ToString("e15", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "2.550000000000000e+2" testCase "float.ToString 'E' works" <| fun () -> @@ -938,6 +938,16 @@ let tests = $"{100000L:E}" |> equal "1.000000E+005" $"{100000L:E2}" |> equal "1.00E+005" + testCase "ToString 'E' keeps every digit of a 64 bit integer" + <| fun () -> + (0L).ToString("E2", CultureInfo.InvariantCulture) |> equal "0.00E+000" + (100000L).ToString("e4", CultureInfo.InvariantCulture) |> equal "1.0000e+005" + (999999L).ToString("E2", CultureInfo.InvariantCulture) |> equal "1.00E+006" + (9223372036854775807L).ToString("E17", CultureInfo.InvariantCulture) + |> equal "9.22337203685477581E+018" + (-9223372036854775808L).ToString("E2", CultureInfo.InvariantCulture) |> equal "-9.22E+018" + (18446744073709551615UL).ToString("E", CultureInfo.InvariantCulture) |> equal "1.844674E+019" + testCase "ToString with a custom percent format multiplies by 100" <| fun () -> (100000).ToString("0.0%", CultureInfo.InvariantCulture) |> equal "10000000.0%" From 036e7acd5234c9b53b419e6c7a0929f1660c714e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:31:48 +0200 Subject: [PATCH 044/322] chore: release multiple projects (#4942) --- src/Fable.AST/CHANGELOG.md | 10 +++++++++- src/Fable.Cli/CHANGELOG.md | 20 +++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 20 +++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 19 ++++++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 15 ++++++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 19 ++++++++++++++++++- src/fable-standalone/package.json | 4 ++-- 10 files changed, 104 insertions(+), 13 deletions(-) diff --git a/src/Fable.AST/CHANGELOG.md b/src/Fable.AST/CHANGELOG.md index b572bfe0ff..cebcd3f4b8 100644 --- a/src/Fable.AST/CHANGELOG.md +++ b/src/Fable.AST/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: b471dc16fc3b5132af77b5974d1669c9b8220cca +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.1.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/b471dc16fc3b5132af77b5974d1669c9b8220cca..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 5.0.0 - 2026-04-21 * Stable release diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 818c709b92..0ed8d23e31 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.17.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 5.16.0 - 2026-09-03 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 0c25319e01..259c4937c0 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.16.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 5.15.0 - 2026-09-03 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 12be0f42a4..4b01aa917d 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.16.0" + let VERSION = "5.17.0" [] - let JS_LIBRARY_VERSION = "2.6.0" + let JS_LIBRARY_VERSION = "2.7.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index d7ea9aa913..3cdeadcc4f 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.3.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 3.2.0 - 2026-09-03 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 7182debb89..68f638dbe4 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.2.0", + "version": "3.3.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.2.0" + "@fable-org/fable-standalone": "^3.3.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 2077552206..1777fcc54e 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 updaters: - package.json: file: package.json @@ -15,6 +15,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.7.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Match .NET for G, E and custom numeric formats ([6fb03090](https://github.com/fable-compiler/Fable/commit/6fb03090ce328b8c5d6a36422f120dd641aefe3a)) +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 2.6.0 - 2026-09-03 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index fc1de0835b..d7387fd226 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.6.0", + "version": "2.7.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index af35e136a4..f6bf96616c 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: bf694309ae5dbc6bb546d9acc4d091f5e35fe330 +last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -19,6 +19,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.3.0 - 2026-09-08 + +### 🚀 Features + +* Add Temporal dates and times (opt-in) (#4759) ([3e8f56f7](https://github.com/fable-compiler/Fable/commit/3e8f56f78345fbee8a4394c5452eb212264701f9)) + +### 🐞 Bug Fixes + +* *(js/ts)* Allow the E format specifier on Int64 and UInt64 ([c49d1ec5](https://github.com/fable-compiler/Fable/commit/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321)) +* *(python)* Build typed arrays from lists and sequences (#4944) ([5af37779](https://github.com/fable-compiler/Fable/commit/5af37779ce33c1e9036df7a9548830c4f914bdd5)) +* *(rust)* Fix IEnumerable casting, interop and type testing (#4946) ([93c2df5b](https://github.com/fable-compiler/Fable/commit/93c2df5ba2b3316893dc85dc4fd746a09a07bf39)) +* *(rust)* Restore missing bindings (#4947) ([2fb46a26](https://github.com/fable-compiler/Fable/commit/2fb46a269bc4827494cab878f6e3f0e1b4052035)) +* *(ts)* Declare local function type params only reachable via constraints (#4943) ([8aa6d3b9](https://github.com/fable-compiler/Fable/commit/8aa6d3b9e9f3c1dc013d67406878ea93637ad74c)) +* *(ts/python)* Erase byref from struct copy-and-update locals (#4945) ([19a6522e](https://github.com/fable-compiler/Fable/commit/19a6522ea517db53dabd5e221206849d382e1025)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/bf694309ae5dbc6bb546d9acc4d091f5e35fe330..c49d1ec5d1a491bfff65fe4bd6c71014cac9b321) + ## 3.2.0 - 2026-09-03 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index f9603b5df7..1a20c1e394 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.2.0", - "fableVersion": "5.16.0", + "version": "3.3.0", + "fableVersion": "5.17.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From fbc162d6912d62b9a75e73f8264814f6519b6194 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 8 Sep 2026 20:04:14 +0200 Subject: [PATCH 045/322] fix(python): preserve record field names in structured formatting (#4952) --- .../Python/Fable2Python.Transforms.fs | 13 +++++++++++ src/fable-library-py/fable_library/record.py | 22 +++++++++++++------ tests/Python/TestRecordType.fs | 5 +++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 9685c83fbf..1c71ea9c0a 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3538,6 +3538,18 @@ let declareDataClassType = let name = com.GetIdentifier(ctx, entName) + // Record slots follow Python naming conventions, but structured formatting must + // retain the original F# field names. Keep the names on the generated class so + // the runtime can pair them with the slots without attempting a lossy conversion. + let fsharpFieldNames = + ent.FSharpFields + |> List.filter (fun field -> not field.IsStatic) + |> List.map (fun field -> Expression.stringConstant field.Name) + |> Expression.tuple + + let fsharpFieldNamesMember = + Statement.assign ([ Expression.name ("__fable_field_names__", Store) ], fsharpFieldNames) + // Generate field annotations from entity's FSharpFields to properly uncurry function types. // Record/dataclass fields store functions uncurried at runtime, so we need to uncurry // lambda types in the type annotations to match. @@ -3661,6 +3673,7 @@ let declareDataClassType let classBody = let body = [ + yield fsharpFieldNamesMember yield! staticFieldAnnotations yield! props yield! classMembers diff --git a/src/fable-library-py/fable_library/record.py b/src/fable-library-py/fable_library/record.py index 12313ed374..6a195f3ad2 100644 --- a/src/fable-library-py/fable_library/record.py +++ b/src/fable-library-py/fable_library/record.py @@ -79,15 +79,23 @@ def _field_to_string(value: object) -> str: def record_to_string(self: Record) -> str: - if hasattr(self, "__slots__"): - return ( - "{ " - + "\n ".join(map(lambda slot: slot + " = " + _field_to_string(getattr(self, slot)), self.__slots__)) - + " }" - ) - else: + if not hasattr(self, "__slots__"): return "{ " + "\n ".join(map(lambda kv: kv[0] + " = " + _field_to_string(kv[1]), self.__dict__.items())) + " }" + slots = self.__slots__ + # Newer generated records retain their source-level F# field names here. + # Fall back to slots so the current runtime remains compatible with code + # compiled before that metadata was emitted. + field_names = getattr(type(self), "__fable_field_names__", slots) + return ( + "{ " + + "\n ".join( + f"{field_name} = {_field_to_string(getattr(self, slot))}" + for slot, field_name in zip(slots, field_names, strict=True) + ) + + " }" + ) + def record_get_hashcode(self: Record) -> int: # Hash each field with `structural_hash` rather than hashing a Python tuple, diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index 0beec0b55f..8d9ef01395 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -61,6 +61,11 @@ type StructRecord = { Size: int; Seed: int; Path: int list } type StructRecordHolder = { Tag: int; Data: StructRecord } +[] +let ``test sprintf formats record fields with their F# names`` () = + sprintf "%A" { Size = 1; Seed = 2; Path = [] } + |> equal "{ Size = 1\n Seed = 2\n Path = [] }" + let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path } let holdStructRecord (data: StructRecord) (path: int list) = From a07ba4ac37f287ca6d9582f44eea4cac7a45c041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:36:23 +0200 Subject: [PATCH 046/322] chore: Bump the all-actions group with 2 updates (#4958) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 23b0becb9a..36add34cf8 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup-cli@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 334b20a88c..b8248fb542 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@49dc896ccb7d6d975514cfe232bcbd68cc12e2c1 # v0.87.9 + uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 1a20ec167911c1a53e0b69ab16c9b7063f905cc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:36:44 +0200 Subject: [PATCH 047/322] chore: Bump the all-pip group with 2 updates (#4957) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 +-- uv.lock | 80 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 72d8353eb7..bf3a155ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.5,<0.17", + "ruff>=0.16.6,<0.17", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.5", "attrs>=26.1.0", "pyright>=1.1.411", - "ty>=0.0.75", + "ty>=0.0.78", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index a168a1df96..6385c35543 100644 --- a/uv.lock +++ b/uv.lock @@ -68,8 +68,8 @@ dev = [ { name = "pydantic", specifier = ">=2.13.5" }, { name = "pyright", specifier = ">=1.1.411" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.5,<0.17" }, - { name = "ty", specifier = ">=0.0.75" }, + { name = "ruff", specifier = ">=0.16.6,<0.17" }, + { name = "ty", specifier = ">=0.0.78" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.5" +version = "0.16.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, - { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, - { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, - { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, - { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, - { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, - { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] name = "ty" -version = "0.0.75" +version = "0.0.78" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/d0/d0c96f898d6974a4a3569ab3efdf9512c04ad99f9203effb55f72497fe97/ty-0.0.75.tar.gz", hash = "sha256:4c5eead33dfbf6e2ebb4f400f74b51ffc9bab702a6f23ddb648a1cbb740387e3", size = 6868326, upload-time = "2026-08-26T20:23:40.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/c7/2ba0861384c5b5097ac354383abb98188112cb330208c21d5197e98a29e5/ty-0.0.78.tar.gz", hash = "sha256:770b45854f85fa11595208f08c0f28df80943164d10a2832d86be6ac29f135b2", size = 7050609, upload-time = "2026-09-02T22:41:33.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/6c/b12d03505f17581f0cfa3c12273fe34c1d67b36dfda1bc561a6bdc16512b/ty-0.0.75-py3-none-linux_armv6l.whl", hash = "sha256:e5409f50db2246fd4bd039d93d261e0cfa1daa554a4fb77256f91072c570349a", size = 12972606, upload-time = "2026-08-26T20:22:59.716Z" }, - { url = "https://files.pythonhosted.org/packages/d1/aa/30f11eecd9215a9f87e8fe8baaf48f3ce905f5d75b8e4aac70f0091f130c/ty-0.0.75-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5e7b8b3472fb9bb2eeab314984b265df08a7a9d518867a9e6020eebc06570be2", size = 12527158, upload-time = "2026-08-26T20:23:02.767Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/7fd7001b0b5c6610bfbad7357e47d5fe6f82d4e84e94c53776a478f5e9f8/ty-0.0.75-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6ccf34169821fe0d23e3360deeef981d217963412f1d087b9bdd32ec57f7a57", size = 12400533, upload-time = "2026-08-26T20:23:04.965Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/1e284ea3d348d7be02f12d83bc22ed9ef193033f863f05b64db99027f141/ty-0.0.75-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842ebb41e9c6c334b40768704e20b1a69d5c6b08805b289d5e0e2565f49f2de1", size = 12420592, upload-time = "2026-08-26T20:23:07.427Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ab/d813271543370c47fd74b5118f2066ab32b0983e907b1821f3f9a6d0fa7f/ty-0.0.75-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7a5a723c5f1e0fab4ffbfe9bd95123a526ed48f206e5f25cb2161ca294007a", size = 12739219, upload-time = "2026-08-26T20:23:09.809Z" }, - { url = "https://files.pythonhosted.org/packages/31/5b/95b49cc5570fd92a7bf63732f649b31906158721e03c7fcb1b5be74ee3bf/ty-0.0.75-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54382f98e5da292fcd7104391afef5105c35bb2f312e29bea6f5fa419935255c", size = 13494046, upload-time = "2026-08-26T20:23:12.191Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/502d2dd68173cf020e1ad2bdbab9544c86776de0b0e2ed15f8c2fe006e3d/ty-0.0.75-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac13b180dc2aade2cd243f56b01650e78bf091a2e522ad3bc947245d7837c613", size = 13938899, upload-time = "2026-08-26T20:23:14.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/5b/f3b12a25c07224456219fc2bd20db0ad7e40b304be0ff6aad728da0135f9/ty-0.0.75-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752df7951a443219d7f1ff817e3723c85d428565ff449e08a7a93ba821661526", size = 13656711, upload-time = "2026-08-26T20:23:17.145Z" }, - { url = "https://files.pythonhosted.org/packages/51/7b/f090ad306e2b15a07b332d647138c5264b89d9758855ecce8b8a10bcb153/ty-0.0.75-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd399feedf7cee816563c1baec45fc1c0b3c89f1ea42364920b688004b5b7da", size = 13093499, upload-time = "2026-08-26T20:23:19.489Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4b/f69b99aaaca0c7c65d5f114b186b26b21666f767b0c69eec99a2bdccc061/ty-0.0.75-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d7625f6f56c7dc1e873579fdc9e432a0e21e302afe847ab60704d2303442a92e", size = 13520580, upload-time = "2026-08-26T20:23:21.789Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/692c5f905c0345a15d2255fc74066d660f030254ae8dcdaf33f5a5c2f279/ty-0.0.75-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:89e7d527e95a2534b70cae29e94c104b84082760ea05927d23bb87280969c104", size = 12524095, upload-time = "2026-08-26T20:23:24.026Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9a/f42b12cf265ea95344bf554764c4791cfb273bdd628aadd7c209af7cadc3/ty-0.0.75-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0843f134440740706e01bee5f88f4cfc10e9b018bddb9e4ef4c12dc9fc0c9aef", size = 12756591, upload-time = "2026-08-26T20:23:26.126Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5e/9b180c133cb9cce48179a7d2bf9e1802d992aa8176a918e0e05205760b42/ty-0.0.75-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1bd0ec0e50ee1376875c88891efe6f549c3560fa5b2ddad79a425cd5a6218b9c", size = 12998754, upload-time = "2026-08-26T20:23:28.353Z" }, - { url = "https://files.pythonhosted.org/packages/39/f6/3c6ef5dd550103e29905121c67fb96a374564f31a2f44c6faa1af98c2d61/ty-0.0.75-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f9eafd561f90110d5e29f589ec3e956c4686e2f6631348d99276436f5cbe4d1", size = 13316474, upload-time = "2026-08-26T20:23:30.857Z" }, - { url = "https://files.pythonhosted.org/packages/bb/52/12776337874c821076bd5368e352ccd9e67174790abe3b856f749cb3524b/ty-0.0.75-py3-none-win32.whl", hash = "sha256:05063a6fafe2154b794a7f964515d148e51acd186d72d4a3acd347ee9fa19336", size = 12316315, upload-time = "2026-08-26T20:23:33.528Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/bb51e16af5c7138c9f52f8f3d0a401a371c6798d092e3b74926f186a9814/ty-0.0.75-py3-none-win_amd64.whl", hash = "sha256:81cf1ba5f6b7536ad56747865214255d9bc8e80533a689dbb9ddeaad464b09f1", size = 12917267, upload-time = "2026-08-26T20:23:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/f34cfc06a9ba72979df219e48dae1a0b6a6c74a340763ccd30a547edf913/ty-0.0.78-py3-none-linux_armv6l.whl", hash = "sha256:122700b98f9d45785c1ce91a9418154562e7f64f4bf34679f6f7b38c5b97453f", size = 13304624, upload-time = "2026-09-02T22:40:56.649Z" }, + { url = "https://files.pythonhosted.org/packages/1c/28/5576e2a08b57676d9b2a736d528f077a6c6e32c3d1de2d75dbbe28346966/ty-0.0.78-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbe7e3709ccf29ef3d9f58f5914cc30ec36fb647008a5a4ff8483abe401bfe8d", size = 12928383, upload-time = "2026-09-02T22:40:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/027d49c3da5235e634da3d3fc52c4874ec89f50830388c9c259f8fb71e0e/ty-0.0.78-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c3528897b3ab9d3589561bc2b5e61a4d5d686de527a4400bb09a439b922a6c70", size = 12730058, upload-time = "2026-09-02T22:41:01.235Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bc/6bf8ffefd8063730a70ae889cf85def72bc155fd1453135ebf8a09c2f1c2/ty-0.0.78-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bfba4c44a06484093f527b8ef43a317abcf91395b49396170266629eedf74aa", size = 12813321, upload-time = "2026-09-02T22:41:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c0/b3cdf26f82108908d92fdb7ddb741019c446ceb666cf204d4dbf611bde33/ty-0.0.78-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f903c06fdee17baf8373173039ab28f1bce28e66b1c734929a5f50b37eb54d9", size = 13072219, upload-time = "2026-09-02T22:41:05.225Z" }, + { url = "https://files.pythonhosted.org/packages/4d/36/16bfb0abdd178dae11dbc4b57b9a86c2b62642a18c1103fd2c2930aa5879/ty-0.0.78-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd83e5fe3f07291d1bd4e591f8d2607c204f3eab53bcb1b8060c40e876e1aa61", size = 13903958, upload-time = "2026-09-02T22:41:07.333Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0c/74d3b1f0344b13156c719dd68a7edf7e48c2051718c9f326ba3cbf45ea53/ty-0.0.78-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325285377319cf7168a2b8b771ae5027f411530e3c7eab1faf4583cdd72fd7c9", size = 14355581, upload-time = "2026-09-02T22:41:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d7/7ca0359e1e61b15b8b02328c8d120db3c507876b9b7c6bd9f26935353e0e/ty-0.0.78-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b7846a404da6492697b524a769755ee9bee157d67674063ecd9c5f69dc52ff", size = 14044749, upload-time = "2026-09-02T22:41:11.678Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/731f16ff42c5fc96e742c2f4e0a6915356bae114ae02ae4af6f33502175f/ty-0.0.78-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:169d3b9d134c0b48fe1af8a844142d374655552054a9d6c15a4b6e51bb0382ea", size = 13391647, upload-time = "2026-09-02T22:41:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/0c/af/250cc29daf310e837188509ea4d78460c495d8a74c4fb84b4152d9ef2d4f/ty-0.0.78-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6108cb3b2d28dac5981d4e25008a0a5547d8c877a67f6da9e8c38e7e946be44f", size = 13945020, upload-time = "2026-09-02T22:41:15.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/96aab1dee4535e66554e8dc7657c69f6c61c181d8e70b9c3527908e93ef4/ty-0.0.78-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:589ad03608d9d2975ef4b23c41c4f847e325bcc7686f51d4c66066b8dbc18a2a", size = 12851149, upload-time = "2026-09-02T22:41:18.06Z" }, + { url = "https://files.pythonhosted.org/packages/85/b0/53d8fe9a847534ef5fe2165c7d5292cb853b29dfd7011cbfb1cdb90a8012/ty-0.0.78-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b3fa0786edc1af06030f872d83499c0cea7c261dbbb651e0aee8306bf2c8a869", size = 13091464, upload-time = "2026-09-02T22:41:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/21/65/94a4e5a02de559f6c6c0ee7a14b88660b4eee058ec6fec5c0ff6ecfbf5cc/ty-0.0.78-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92c5639befc577578c8abd4e5a7fccf98d828db98b09e8d4dd81605dc0e54aef", size = 13389389, upload-time = "2026-09-02T22:41:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/d0c7fe6be64ca5a15100c4b1f0e39c19660d53bc544f609fa117b346e661/ty-0.0.78-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0dce70bc51652b2775debd1d1e422fb0179f5207c73deb4d5d0aca37e5f61695", size = 13691928, upload-time = "2026-09-02T22:41:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5d/36081205611ba3fa802efc4ad63e4b1e949bda2f7bb37b34ac9bb6c00db0/ty-0.0.78-py3-none-win32.whl", hash = "sha256:1c80976ca9185d7a9d1baab1fb57240331b8dac58d3b11e46200284086a6de8d", size = 12647346, upload-time = "2026-09-02T22:41:26.699Z" }, + { url = "https://files.pythonhosted.org/packages/f1/89/b925fe1ea1bc56fc7f11d2496e29072fdd2ceb7b389884fc7b2d07cf0c41/ty-0.0.78-py3-none-win_amd64.whl", hash = "sha256:32e82b704471eab34f67b51c151660ca8a00815977b28278905d76fba54f7415", size = 13241810, upload-time = "2026-09-02T22:41:28.857Z" }, + { url = "https://files.pythonhosted.org/packages/91/f1/090ef7b52355bcedfbfbff6ce70fa81ba5bd5f6ed3f8d99ae05e6f2fe75b/ty-0.0.78-py3-none-win_arm64.whl", hash = "sha256:3a14d641a3c04fa9a80f2a46be1531d915f60d4fb79d4b894627bbe46bb35d64", size = 13077433, upload-time = "2026-09-02T22:41:31.525Z" }, ] [[package]] From 4befee41416b72b11115e2bce1ab5222c692cc0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:37:15 +0200 Subject: [PATCH 048/322] chore: Bump the all-npm group in /tests/React with 3 updates (#4955) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 632 +++++++++++++++++----------------- tests/React/package.json | 2 +- 2 files changed, 317 insertions(+), 317 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index a309f88fa2..f39e6ef45d 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -10,7 +10,7 @@ "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", - "@testing-library/user-event": "^14.6.6", + "@testing-library/user-event": "^14.6.7", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", @@ -1035,16 +1035,16 @@ } }, "node_modules/@jest/console": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.0.tgz", - "integrity": "sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.1.tgz", + "integrity": "sha512-u5Ncuc+gXVUwjNMFQOnphHo2Qx2DxyC8Dvpmf4HCx4y0kvSkRRNiQYRixrvOP4V7y52JcSuNxqochCt0PpdHVg==", "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.5.0", - "jest-util": "30.5.0", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -1052,17 +1052,17 @@ } }, "node_modules/@jest/core": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.0.tgz", - "integrity": "sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.1.tgz", + "integrity": "sha512-BL9g6CJUUhIbdoAflz/Va658erSSXUIvU8XUYiWNTbZljJjZ5yaC9EJ9/dQ19rpbYV3ZOK4sBACqhPaTyhoUIA==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.0", + "@jest/console": "30.5.1", "@jest/pattern": "30.5.0", - "@jest/reporters": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", + "@jest/reporters": "30.5.1", + "@jest/test-result": "30.5.1", + "@jest/transform": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", @@ -1070,20 +1070,20 @@ "exit-x": "^0.2.2", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.5.0", - "jest-config": "30.5.0", - "jest-haste-map": "30.5.0", - "jest-message-util": "30.5.0", + "jest-changed-files": "30.5.1", + "jest-config": "30.5.1", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-resolve-dependencies": "30.5.0", - "jest-runner": "30.5.0", - "jest-runtime": "30.5.0", - "jest-snapshot": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", - "jest-watcher": "30.5.0", - "pretty-format": "30.5.0", + "jest-resolve": "30.5.1", + "jest-resolve-dependencies": "30.5.1", + "jest-runner": "30.5.1", + "jest-runtime": "30.5.1", + "jest-snapshot": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", + "jest-watcher": "30.5.1", + "pretty-format": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -1111,9 +1111,9 @@ } }, "node_modules/@jest/core/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -1135,32 +1135,32 @@ } }, "node_modules/@jest/environment": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.0.tgz", - "integrity": "sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.1.tgz", + "integrity": "sha512-eYJAkOsrpwDXPcoLNEG6lN9Zo3Cy35pxnVQD74vyIfsi/Q8wB/lZFsEjU4wrecOgT4ZviQDZaX/cFIJyMdMxTw==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.5.0", - "@jest/types": "30.5.0", + "@jest/fake-timers": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.0" + "jest-mock": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.0.tgz", - "integrity": "sha512-825vac4Dmysbn2kU7VUQPoKuj/HNUpSTgv98KCByMOSPvHuj1/HpVZeLRsP/itDB2HFiDcoTUrsg8fSu3PxKBw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.1.tgz", + "integrity": "sha512-J395vmP3Fb2Te0JmF7pe4si4jpfbXef1YsY4UpHYL6OOxS2molu9Dsie1VmIiUalXdtmz1P5QRgc+5hBD+ssBg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/fake-timers": "30.5.0", - "@jest/types": "30.5.0", + "@jest/environment": "30.5.1", + "@jest/fake-timers": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.0", - "jest-util": "30.5.0" + "jest-mock": "30.5.1", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1177,22 +1177,22 @@ } }, "node_modules/@jest/expect": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.0.tgz", - "integrity": "sha512-jEmgmgJEobJ3zEhDOGp1VAJ6JkoVelpS8uZ1ae1Ul/5lP78UKJKmmU0lciJwd6JdnqOXHaaS/QCKwbf1dHI9MA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.1.tgz", + "integrity": "sha512-uOGd40P/COyUp9xHf5jeGiGJC2/ANg+2+Tk9/xN5/LxmlY/r/gxsPrx3DTEtaRZsLAX4wgNSLEDELZ5bmVs2bA==", "license": "MIT", "dependencies": { - "expect": "30.5.0", - "jest-snapshot": "30.5.0" + "expect": "30.5.1", + "jest-snapshot": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.0.tgz", - "integrity": "sha512-5j0ztPxSy3McUJihjkDdCyCfjvT2hxykFTWsgEBZKB8qsw9ALdCiGTpTRH5gnf/d+qI4SflYUJ0dWNbzjQCWbA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.1.tgz", + "integrity": "sha512-WcRWhHQdTMRDpyWKZ/6MINBmovI7zeD+bL8wFjCncRV3NQOwKy1X45IfyblfHR4k/XciIlNEdFL9QjFO+HNKOg==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0" @@ -1202,17 +1202,17 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.0.tgz", - "integrity": "sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.1.tgz", + "integrity": "sha512-rEkV6YzpBXo/L9cnj2ibyuYZuyGiNWaPUsyCu3HYJbqCV4jnEiKmf7hId20z8eKjZ0JQ9jtIYKxRSmYB/OYSsA==", "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", - "jest-message-util": "30.5.0", - "jest-mock": "30.5.0", - "jest-util": "30.5.0" + "jest-message-util": "30.5.1", + "jest-mock": "30.5.1", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1228,15 +1228,15 @@ } }, "node_modules/@jest/globals": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.0.tgz", - "integrity": "sha512-h7eJx534czwL8lQMYB0hwLT4/HquO8EX/RtYL7RNUHyUyWWVciYjoudN4Ns5JmNvn2/jh0Vm9UstZjEzJJ5EsQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.1.tgz", + "integrity": "sha512-VhqvQ251XIC7pk46YymI3HCeBLOdvriDw9zibekodAHEwoVvbVVgpU5H13GvmyOAzxtZCfsm1uvzqkaqJf2I+g==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/expect": "30.5.0", - "@jest/types": "30.5.0", - "jest-mock": "30.5.0" + "@jest/environment": "30.5.1", + "@jest/expect": "30.5.1", + "@jest/types": "30.5.1", + "jest-mock": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1270,16 +1270,16 @@ "license": "MIT" }, "node_modules/@jest/reporters": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.0.tgz", - "integrity": "sha512-FEAuusWm+PUOn9ydjaHhpOpyPmS6IbGE04HaKTuUI4zd8eqYZiXiNLoCsdCog/l2XnNj53E9pFy64UltcvfJKg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.1.tgz", + "integrity": "sha512-RbUXIfv85KxitJn4l3MpAoMilkvXe2QCO5lXxHWftywo/VdZ5vkEoHRDgphcxP6qmoIkUaN8/UZj6NhdkIFdJg==", "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", + "@jest/console": "30.5.1", + "@jest/test-result": "30.5.1", + "@jest/transform": "30.5.1", + "@jest/types": "30.5.1", "@jridgewell/trace-mapping": "^0.3.31", "@types/node": "*", "chalk": "^4.1.2", @@ -1292,9 +1292,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.5.0", - "jest-util": "30.5.0", - "jest-worker": "30.5.0", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1324,12 +1324,12 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.0.tgz", - "integrity": "sha512-iWQtIsi2dRsO2oWzVceOeynuRJiYTW8gsDVp5wFQ02ipHluQsNgBpasWSHiawVxukIlsGLdCtCSbIEK7fPtpvQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz", + "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==", "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1354,13 +1354,13 @@ } }, "node_modules/@jest/test-result": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.0.tgz", - "integrity": "sha512-9IlPqUzUMkVDmoDqSSrVVLroVotgN3hTUPWPwq24XWXhh1Zpg915RXZ5pgRJ/7j4YE/kng5nqjSn4p3S68SZJA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.1.tgz", + "integrity": "sha512-A/1S6ZBdpic50E0pxLgvaB9XNPL4k7AksmG69OO2oiotxciWZwHkbOec+qbIi+uUtIIByOVlXJUl97oZUsz+Jw==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.0", - "@jest/types": "30.5.0", + "@jest/console": "30.5.1", + "@jest/types": "30.5.1", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1369,14 +1369,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.0.tgz", - "integrity": "sha512-TXlvSDIVv482b83hD8A8WtxDJEmGF47f60W6jRXOQL0Isfs3hKtk4rZIm9R/jLzAibg8+c56y+Q00BQs8R7Xcg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.1.tgz", + "integrity": "sha512-SHcPnrjdVRYJv6y6l4JUTy4Jccu7zmO6BmiOfOA34UiVBto22s19WiDC0A/2qfM7MWB/qdcoqfSIRMitpjTT4A==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.5.0", + "@jest/test-result": "30.5.1", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", + "jest-haste-map": "30.5.1", "slash": "^3.0.0" }, "engines": { @@ -1384,22 +1384,22 @@ } }, "node_modules/@jest/transform": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.0.tgz", - "integrity": "sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.1.tgz", + "integrity": "sha512-EDnDhn0jleU9ZhpVoA4gvqw+Ev0iw/r5upNT2b79RiwiaTiYAMMhvNJP3lmocjIe5J2ZkoNr0B3K/J6O5GIm4Q==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@jridgewell/trace-mapping": "^0.3.31", "babel-plugin-istanbul": "^8.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", + "jest-haste-map": "30.5.1", "jest-regex-util": "30.5.0", - "jest-util": "30.5.0", + "jest-util": "30.5.1", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -1409,9 +1409,9 @@ } }, "node_modules/@jest/types": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.0.tgz", - "integrity": "sha512-s1N+79S4Yp9ZgklCauZXi+YPJdCdtStNYQT32stuD6EeQaIBGHoUfyj2P0YWy8RmuQfaJboO+ulxEvEheR/POQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", "license": "MIT", "dependencies": { "@jest/pattern": "30.5.0", @@ -1927,9 +1927,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.6", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", - "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", "license": "MIT", "engines": { "node": ">=12", @@ -2507,12 +2507,12 @@ } }, "node_modules/babel-jest": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.0.tgz", - "integrity": "sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.1.tgz", + "integrity": "sha512-ge1xUVZS91ml09YRMgRGgeKJ4YJpcOiuwteAxFBYLugQyp7cRw+hHej6Ho0vPjvLrjq60bb7JPHH9LAMA1/krA==", "license": "MIT", "dependencies": { - "@jest/transform": "30.5.0", + "@jest/transform": "30.5.1", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^8.0.0", "babel-preset-jest": "30.5.0", @@ -3067,17 +3067,17 @@ } }, "node_modules/expect": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.0.tgz", - "integrity": "sha512-8fiMWcEjPU7B9nErC4FtFcCzf2tC6I75Qf7m8wzBAWC2taZmcno3yAFEjIQL34SwoGZNgPf63UDiJLyh4SMPaw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.1.tgz", + "integrity": "sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.5.0", + "@jest/expect-utils": "30.5.1", "@jest/get-type": "30.5.0", - "jest-matcher-utils": "30.5.0", - "jest-message-util": "30.5.0", - "jest-mock": "30.5.0", - "jest-util": "30.5.0" + "jest-matcher-utils": "30.5.1", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.1", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3492,15 +3492,15 @@ } }, "node_modules/jest": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.0.tgz", - "integrity": "sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.1.tgz", + "integrity": "sha512-3qrR8+ZXFnn7y0H2yjWQNkGGBLBY4zTRoTMAq9zJcgwLLtlyonfsCLviIXK9xuE2KgIyM+M36AFcoK2DgFR36w==", "license": "MIT", "dependencies": { - "@jest/core": "30.5.0", - "@jest/types": "30.5.0", + "@jest/core": "30.5.1", + "@jest/types": "30.5.1", "import-local": "^3.2.0", - "jest-cli": "30.5.0" + "jest-cli": "30.5.1" }, "bin": { "jest": "bin/jest.js" @@ -3518,13 +3518,13 @@ } }, "node_modules/jest-changed-files": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.0.tgz", - "integrity": "sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz", + "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==", "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.5.0", + "jest-util": "30.5.1", "p-limit": "^3.1.0" }, "engines": { @@ -3532,28 +3532,28 @@ } }, "node_modules/jest-circus": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.0.tgz", - "integrity": "sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.1.tgz", + "integrity": "sha512-NgliezXQ6yznqR4W5Gqw++0cZSbBZlF0NknNIAE5VmSJKWOtmWJmWnBq3TSkUOVBTPrT7FGGhVdA8DLWnxo2Sw==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/expect": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/types": "30.5.0", + "@jest/environment": "30.5.1", + "@jest/expect": "30.5.1", + "@jest/test-result": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.5.0", - "jest-matcher-utils": "30.5.0", - "jest-message-util": "30.5.0", - "jest-runtime": "30.5.0", - "jest-snapshot": "30.5.0", - "jest-util": "30.5.0", + "jest-each": "30.5.1", + "jest-matcher-utils": "30.5.1", + "jest-message-util": "30.5.1", + "jest-runtime": "30.5.1", + "jest-snapshot": "30.5.1", + "jest-util": "30.5.1", "p-limit": "^3.1.0", - "pretty-format": "30.5.0", + "pretty-format": "30.5.1", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3575,9 +3575,9 @@ } }, "node_modules/jest-circus/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3590,20 +3590,20 @@ } }, "node_modules/jest-cli": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.0.tgz", - "integrity": "sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.1.tgz", + "integrity": "sha512-uwNYepWgaNBCplm42fCIUZTf/tIEHIy5AYvx7eL1BrwIgyjPt+2poouR23UO2yopIP+kV8oZFHfv+l4pg/8prQ==", "license": "MIT", "dependencies": { - "@jest/core": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/types": "30.5.0", + "@jest/core": "30.5.1", + "@jest/test-result": "30.5.1", + "@jest/types": "30.5.1", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", + "jest-config": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "yargs": "^17.7.2" }, "bin": { @@ -3622,32 +3622,32 @@ } }, "node_modules/jest-config": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.0.tgz", - "integrity": "sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.1.tgz", + "integrity": "sha512-L8PKM2X/ngG8PxfLMqglKGZjylPgw84bVPUlMY9W/o76TnLqPWrmQgsaT0HrheGdRtpGE5FbmREA0Kvb+Qx5gQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.5.0", "@jest/pattern": "30.5.0", - "@jest/test-sequencer": "30.5.0", - "@jest/types": "30.5.0", - "babel-jest": "30.5.0", + "@jest/test-sequencer": "30.5.1", + "@jest/types": "30.5.1", + "babel-jest": "30.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-circus": "30.5.0", + "jest-circus": "30.5.1", "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.0", + "jest-environment-node": "30.5.1", "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-runner": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", + "jest-resolve": "30.5.1", + "jest-runner": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "parse-json": "^5.2.0", - "pretty-format": "30.5.0", + "pretty-format": "30.5.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3684,9 +3684,9 @@ } }, "node_modules/jest-config/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3699,15 +3699,15 @@ } }, "node_modules/jest-diff": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.0.tgz", - "integrity": "sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.1.tgz", + "integrity": "sha512-e3cNNMpv8Kh20MjjphTXs+3Vz7DQyLM1nft7KJhnh46atFhjVJRa+0Hq0beywuwsACtMQUBihQkFl8zxb7gt1Q==", "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.5.0", "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "pretty-format": "30.5.0" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3726,9 +3726,9 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3753,16 +3753,16 @@ } }, "node_modules/jest-each": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.0.tgz", - "integrity": "sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.1.tgz", + "integrity": "sha512-S1af0TU4v1EZ/AUlkFs/sxf/5KGsbAT9kRgdyoMX/x72y7C8ZEgETE5o1TPnbKRnrbprc5FR/moYLfdRmqEjsQ==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "chalk": "^4.1.2", - "jest-util": "30.5.0", - "pretty-format": "30.5.0" + "jest-util": "30.5.1", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3781,9 +3781,9 @@ } }, "node_modules/jest-each/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3796,13 +3796,13 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.0.tgz", - "integrity": "sha512-VVHN/G3zrxsQR398jvMalM76ALX6YBAftsYLCGtTeKRmz4f42YJAP05AGpk0VF5SLtoHdkfKunYGfnKPnmcEOA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.1.tgz", + "integrity": "sha512-8lzKbC/SRbQE24wr1OOJV+aYtDAuVNKBryN6YcFiCcaZZ3I7grcZY7w91BwNvGET0ubKDmomEHZFHMaF+6pAlA==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/environment-jsdom-abstract": "30.5.0", + "@jest/environment": "30.5.1", + "@jest/environment-jsdom-abstract": "30.5.1", "@types/jsdom": "^21.1.7", "jsdom": "^26.1.0" }, @@ -3819,30 +3819,30 @@ } }, "node_modules/jest-environment-node": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.0.tgz", - "integrity": "sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.1.tgz", + "integrity": "sha512-LrPj3sPMjsQoOB3jrb8p/sa+XkSFNKo60TTsyc+EB2kxQJHgbwElpXnx1yX25fdFn5958FIObRtcLSHyV8VIAw==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/fake-timers": "30.5.0", - "@jest/types": "30.5.0", + "@jest/environment": "30.5.1", + "@jest/fake-timers": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0" + "jest-mock": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.0.tgz", - "integrity": "sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz", + "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==", "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@parcel/watcher": "^2.6.0", "@types/node": "*", "anymatch": "^3.1.3", @@ -3850,8 +3850,8 @@ "fdir": "^6.5.0", "graceful-fs": "^4.2.11", "jest-regex-util": "30.5.0", - "jest-util": "30.5.0", - "jest-worker": "30.5.0", + "jest-util": "30.5.1", + "jest-worker": "30.5.1", "picomatch": "^4.0.3" }, "engines": { @@ -3859,13 +3859,13 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.0.tgz", - "integrity": "sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz", + "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", - "pretty-format": "30.5.0" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3884,9 +3884,9 @@ } }, "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3899,15 +3899,15 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.0.tgz", - "integrity": "sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.1.tgz", + "integrity": "sha512-aroZVqwOz/wC2y6pC+obgFWKV9viaQWQTTSB6W5H55+egtUczIfXR/rxExTv92xD/ADYwpqaYfVWx1aqwKg7FA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "jest-diff": "30.5.0", - "pretty-format": "30.5.0" + "jest-diff": "30.5.1", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3926,9 +3926,9 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3941,19 +3941,19 @@ } }, "node_modules/jest-message-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.0.tgz", - "integrity": "sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", + "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-util": "30.5.0", + "jest-util": "30.5.1", "picomatch": "^4.0.3", - "pretty-format": "30.5.0", + "pretty-format": "30.5.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -3974,9 +3974,9 @@ } }, "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -3989,15 +3989,15 @@ } }, "node_modules/jest-mock": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.0.tgz", - "integrity": "sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.1.tgz", + "integrity": "sha512-9fVjc3leUpGID2/by/LU4Dvdcp7PFh9LlxS3QRWK3ABm+KtvEVsG/AEGeLY3gKOZsjkBxyfwGltoAVlW7dygHg==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.5.0", - "@jest/types": "30.5.0", + "@jest/expect-utils": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-util": "30.5.0" + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4013,16 +4013,16 @@ } }, "node_modules/jest-resolve": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.0.tgz", - "integrity": "sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz", + "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", - "jest-util": "30.5.0", - "jest-validate": "30.5.0", + "jest-haste-map": "30.5.1", + "jest-util": "30.5.1", + "jest-validate": "30.5.1", "slash": "^3.0.0", "unrs-resolver": "^1.12.1" }, @@ -4031,45 +4031,45 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.0.tgz", - "integrity": "sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.1.tgz", + "integrity": "sha512-JKpXGONDcTaunrNVn8KCl6qAnwl06jIkvv70lhq0Ze47lsPx9sH5HoeEUiXX6iFGnliHN/qpIbQ0l38wrVmXaw==", "license": "MIT", "dependencies": { "jest-regex-util": "30.5.0", - "jest-snapshot": "30.5.0" + "jest-snapshot": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.0.tgz", - "integrity": "sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.1.tgz", + "integrity": "sha512-FPlQE4+mwnFxXmpPrSi836KV2ZzvK1g6/nPCT8o5BcoDUUNJQeHo1/Qdkoe/QeoL4M7OeJpbnGUhYKhC1VMdaQ==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.0", - "@jest/environment": "30.5.0", + "@jest/console": "30.5.1", + "@jest/environment": "30.5.1", "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", + "@jest/test-result": "30.5.1", + "@jest/transform": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.0", - "jest-haste-map": "30.5.0", - "jest-leak-detector": "30.5.0", - "jest-message-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-runtime": "30.5.0", - "jest-util": "30.5.0", - "jest-watcher": "30.5.0", - "jest-worker": "30.5.0", + "jest-environment-node": "30.5.1", + "jest-haste-map": "30.5.1", + "jest-leak-detector": "30.5.1", + "jest-message-util": "30.5.1", + "jest-resolve": "30.5.1", + "jest-runtime": "30.5.1", + "jest-util": "30.5.1", + "jest-watcher": "30.5.1", + "jest-worker": "30.5.1", "p-limit": "^3.1.0" }, "engines": { @@ -4077,18 +4077,18 @@ } }, "node_modules/jest-runtime": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.0.tgz", - "integrity": "sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.1.tgz", + "integrity": "sha512-UB88+NRkK2Tw/OqV7dofYcyiUGrVZtD41k/N0xQOs9fG//57XHK7JLWG52HD1UYpUAhjK4xm6DBvFX3yWudsMA==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.0", - "@jest/fake-timers": "30.5.0", - "@jest/globals": "30.5.0", + "@jest/environment": "30.5.1", + "@jest/fake-timers": "30.5.1", + "@jest/globals": "30.5.1", "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", + "@jest/test-result": "30.5.1", + "@jest/transform": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.2.0", @@ -4096,13 +4096,13 @@ "es-module-lexer": "^2.1.0", "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.0", - "jest-message-util": "30.5.0", - "jest-mock": "30.5.0", + "jest-haste-map": "30.5.1", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.1", "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.0", - "jest-snapshot": "30.5.0", - "jest-util": "30.5.0", + "jest-resolve": "30.5.1", + "jest-snapshot": "30.5.1", + "jest-util": "30.5.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4111,9 +4111,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.0.tgz", - "integrity": "sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.1.tgz", + "integrity": "sha512-cNWFdSb5xuDGl8hKkAZJ3YtI/PzHpAPFV+HUXWIOG8rMhpDTLVbvAc2d2wRicoYw2wGsJGLaurJT6BLC97bLXQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -4121,20 +4121,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.5.0", + "@jest/expect-utils": "30.5.1", "@jest/get-type": "30.5.0", - "@jest/snapshot-utils": "30.5.0", - "@jest/transform": "30.5.0", - "@jest/types": "30.5.0", + "@jest/snapshot-utils": "30.5.1", + "@jest/transform": "30.5.1", + "@jest/types": "30.5.1", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.5.0", + "expect": "30.5.1", "graceful-fs": "^4.2.11", - "jest-diff": "30.5.0", - "jest-matcher-utils": "30.5.0", - "jest-message-util": "30.5.0", - "jest-util": "30.5.0", - "pretty-format": "30.5.0", + "jest-diff": "30.5.1", + "jest-matcher-utils": "30.5.1", + "jest-message-util": "30.5.1", + "jest-util": "30.5.1", + "pretty-format": "30.5.1", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4155,9 +4155,9 @@ } }, "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -4182,12 +4182,12 @@ } }, "node_modules/jest-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.0.tgz", - "integrity": "sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", "license": "MIT", "dependencies": { - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4199,17 +4199,17 @@ } }, "node_modules/jest-validate": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.0.tgz", - "integrity": "sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz", + "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", - "@jest/types": "30.5.0", + "@jest/types": "30.5.1", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.5.0" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4240,9 +4240,9 @@ } }, "node_modules/jest-validate/node_modules/pretty-format": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.0.tgz", - "integrity": "sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "license": "MIT", "dependencies": { "@jest/react-is-18": "npm:react-is@^18.3.1", @@ -4255,18 +4255,18 @@ } }, "node_modules/jest-watcher": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.0.tgz", - "integrity": "sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.1.tgz", + "integrity": "sha512-+FHJ7C+S7b3ySfhA1aFmoa6TztsnwZVv84ycPRn0tVN93np2EU4b8C/KadqA3l6igdjgLBTnUotab9ER0HLG8A==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.5.0", - "@jest/types": "30.5.0", + "@jest/test-result": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.5.0", + "jest-util": "30.5.1", "string-length": "^4.0.2" }, "engines": { @@ -4274,14 +4274,14 @@ } }, "node_modules/jest-worker": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.0.tgz", - "integrity": "sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz", + "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==", "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.5.0", + "jest-util": "30.5.1", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -4595,9 +4595,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.26", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.26.tgz", - "integrity": "sha512-LPmrA6J31t9ac9294T/WZV6AK5CUcFZ+VXQ7pNNc1+JAjeKthW7vqekOBu6pXzsxD7slwJ7M4XMhamojG3T4oA==", + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz", + "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==", "license": "MIT" }, "node_modules/onetime": { diff --git a/tests/React/package.json b/tests/React/package.json index dd9e3f6c4c..309ab4c568 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -12,7 +12,7 @@ "@fluentui/react": "^8.125.7", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", - "@testing-library/user-event": "^14.6.6", + "@testing-library/user-event": "^14.6.7", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", From d409bd989d79ed50d04c1e20e3034e575c060f05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:37:41 +0200 Subject: [PATCH 049/322] chore: Bump hypothesis from 6.167.0 to 6.167.1 in /src/fable-library-py in the all-pip group (#4956) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 164 +++++++++++++++++------------------ 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index 35a1de74c7..19fc7e61d1 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,94 +61,94 @@ dev = [ [[package]] name = "hypothesis" -version = "6.167.0" +version = "6.167.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/74/e8d1f7080bdaa20daafc7e301075660576d6dae416d0e415df584f1517f0/hypothesis-6.167.0.tar.gz", hash = "sha256:23a60707a2bc5e0c59ef95e94264c4e8fd1c54b4ca1b95cbde74fb68b9c2eeef", size = 505791, upload-time = "2026-08-30T02:06:27.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8cee74c1390b2932406faaab76980f18946f258fa5a8afca17189b3bc655/hypothesis-6.167.1.tar.gz", hash = "sha256:62eefcb4d2791423626e9901c3027a6e0c5ffda2ac0b44b3c7e797ab9d2d5a4c", size = 505849, upload-time = "2026-08-30T19:53:09.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/81/36552b61148d1a8f7659ea5ff0a6ee9a430cbdcc370f686e797307c8634f/hypothesis-6.167.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dc30b21a0b9c0f1af43c6c2c53c009bbf08580e904f1f108bf4d13650a7c7784", size = 785398, upload-time = "2026-08-30T02:05:01.993Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/30558f078b9a3edf4cfb43c95ed7d71956e149a0c9cd84290c766e7ebb33/hypothesis-6.167.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:014b237fe33d9abf50a4bbdc1d9de04307a768bf0fee43422f541169b91eae53", size = 780984, upload-time = "2026-08-30T02:04:33.175Z" }, - { url = "https://files.pythonhosted.org/packages/ad/20/998a8b2c3a457f7303f3d96a7a7542fadc0addd8bc673a1ab2f93bc5a82f/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d3b8435c73a4502e57e757dd80eac73bbc0c9664b0149a9e31250d0711753c8", size = 1117782, upload-time = "2026-08-30T02:05:31.145Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6c/a97cb14e548b5b0d891ed4706d12445f896133df3198ade45e6d8a42b2d3/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34270e93976cf038852a990836a45f9df55883ee8d661131497ec5a1a067af37", size = 1141560, upload-time = "2026-08-30T02:04:16.144Z" }, - { url = "https://files.pythonhosted.org/packages/d6/0f/ebce0b534dff576d9de505084f9e940ea506567c2c0dd06bdb68dcbc2749/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95359d9dbe8057b4ed6ef51217540357303e2e558c61f1a2eaa641362648b7c", size = 1143210, upload-time = "2026-08-30T02:05:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0c/26aa55d3823c5a0f9c862794100f371210214663cd909ad297780543077e/hypothesis-6.167.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4346b4ab05c59613f828452bbf3c8165d31536141dfaece9ad22f9c56b23310d", size = 1163270, upload-time = "2026-08-30T02:05:15.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3d/f4a4ac1919ce29fbdcb4a5db3160690d7c35c47480c6e922386536930a43/hypothesis-6.167.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:31bdca9e569d28a2b17399163a150d7de51fe3d3161203087ee9ee34cba13390", size = 1122975, upload-time = "2026-08-30T02:05:45.643Z" }, - { url = "https://files.pythonhosted.org/packages/6d/65/49633fe583d8470d40d19ad369990ae20068aa87abc185ebc2201631460b/hypothesis-6.167.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7b8658329582e1dddee4f0a563ac53c6ed7262a52e1eebbd72810a443ea6a19", size = 1153926, upload-time = "2026-08-30T02:04:40.492Z" }, - { url = "https://files.pythonhosted.org/packages/42/98/266c863d9bb2eb2a7541016d33e36f713a0c10ac39740ff603933830f632/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1f9c13b3680e37f30184ec9a9fc19d99eaf2278313252a6bc1c51a5fce5b2be4", size = 1293647, upload-time = "2026-08-30T02:04:46.19Z" }, - { url = "https://files.pythonhosted.org/packages/88/0a/7645a13a3925898e0a46d8d07ae878777a84d5e3338a88320cab1e3b21ef/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5ad2bafeb2193981c5cc44490b4f329dd435b1c76c981844cd3549f031481bb2", size = 1419198, upload-time = "2026-08-30T02:06:05.889Z" }, - { url = "https://files.pythonhosted.org/packages/59/f5/f6fd5397ec7d4cbebcefa018680207dcc2f25d05b312fd4c66fc54b6436a/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:c27f11ee4a13960ea64f58b744ee134a50c160b83b8d3de24e7bb744f302fe0a", size = 1274832, upload-time = "2026-08-30T02:05:53.085Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c8/6cdc6e4e5f325f513f8d2672fbc0bb3c83cfbbac710080016db4729708c1/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:42be1bf942e4279892a9af46745363d053931daf4aef50634789a2301bcbed78", size = 1294087, upload-time = "2026-08-30T02:06:23.468Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d0/9ed25a5a6f4206e0c244f1c3949a2522c203eb624425101c30cf47e53452/hypothesis-6.167.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b7ecfb0d63107fad49e56b9ccdb64e76c37ca96559509c8114ed68a27affa5bf", size = 1329934, upload-time = "2026-08-30T02:05:03.719Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/9e0eac0dea23aab5b1331673fcbd1450650a791852d5ec59a6e0df48a606/hypothesis-6.167.0-cp310-abi3-win32.whl", hash = "sha256:5024a40cd2c82f7391499d5348c7cbf0360f6e6d3a116fb9020aa48f5a212483", size = 671853, upload-time = "2026-08-30T02:04:26.692Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6d/bf40b243c0a569026696173a817e7953067959e103551374d08aa2e8d48e/hypothesis-6.167.0-cp310-abi3-win_amd64.whl", hash = "sha256:f0301a5b9c04b6ec7b642e22dd581b64addfc1c07e9188e10394c94816c9b059", size = 678555, upload-time = "2026-08-30T02:06:14.335Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d1/3db36d9d71f291b96fcfb9ba68d696a046ac6723a69bad5e718c520e66dc/hypothesis-6.167.0-cp310-abi3-win_arm64.whl", hash = "sha256:cf4ee1dd5eb44f15c221b7bebab5c9f823e3593f6e09c3adb6ad8cc4af5f1eb0", size = 676571, upload-time = "2026-08-30T02:05:51.208Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f1/9beb0a073731080a61e4b3c6dde7e8993a02e782bcb2f4e648ae85f5a78d/hypothesis-6.167.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3796498cae6894e737d8569d8fe9d3be0650019ca6f378184d18f14cd522c6fd", size = 786102, upload-time = "2026-08-30T02:05:41.575Z" }, - { url = "https://files.pythonhosted.org/packages/02/36/170fdd82d69ccb45be8b498ac5f1ba3d50c512d3d670fd3d47dcba82ed0e/hypothesis-6.167.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be0d0543e767ad1341736cc6e279a4e27de7f7ccbd62ca0fd0b4a5bd293f12d2", size = 781820, upload-time = "2026-08-30T02:04:51.055Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/9a60a32ad04f86296995e50e1eeb51654af6e9e19399e330e8be9bae9ba6/hypothesis-6.167.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c33f3d7c817e42d6f2c39199fdf94d54593b935df02e177ad26a2b5dbd64fbe3", size = 1118075, upload-time = "2026-08-30T02:05:34.521Z" }, - { url = "https://files.pythonhosted.org/packages/f5/85/43ac592a84701751e7eb9f9b0c8d6659afa4e8a512e17e87f3f84ec039f5/hypothesis-6.167.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25434371146b9d59a552ff0bbaba5ec198cbbab5fe24448b20a1df6b220a660b", size = 1163859, upload-time = "2026-08-30T02:04:58.809Z" }, - { url = "https://files.pythonhosted.org/packages/85/06/d40a76e3a1295b1442c3e96ec7d422fb08a7342ccd7ab3f9faacf37b67de/hypothesis-6.167.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8decf66c7027dfa06b6e6bb9cd0ea0f19ae1fe12e9fa5c7b691e5d237c6622fd", size = 1294166, upload-time = "2026-08-30T02:04:11.982Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b5/6deaf923e4fb780719cabb80eef796294f3ec6905a701fe78e5c8ef912a4/hypothesis-6.167.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9315a9adfa59a1c0a9ae0d41e7fc6fdb826cd09a8e1aa3fd3e07a042fd75ffe", size = 1330619, upload-time = "2026-08-30T02:05:27.561Z" }, - { url = "https://files.pythonhosted.org/packages/55/83/61895673efc7796316bf2f8087b0b12691798d16ab822ad151b42e290952/hypothesis-6.167.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b5a5f27ae48453ec2296f7a97600e965d4982bfcce772a66d12204672237848", size = 678501, upload-time = "2026-08-30T02:06:03.868Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c7/cad125cdd10c1eb16294ec0fd245d0ece2fd5fdffc9774499df6cd337551/hypothesis-6.167.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2149948b652a74795cd5b92b624624ff1228f29f2837b1f354a968178ac82860", size = 785868, upload-time = "2026-08-30T02:04:21.199Z" }, - { url = "https://files.pythonhosted.org/packages/9d/62/84f3ac632d2a0d1d9ccb08f75677b610f5b7bea99b75b2a03f92e626d3a3/hypothesis-6.167.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23110d0cafd4bb146b30903f1ce268dd98784ab3a49e7e420e67d0c94f2cac98", size = 781646, upload-time = "2026-08-30T02:05:43.783Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/5a8315cfc24edf7ae2f5706752f28d5b6ebc6f94f7d176bd27faa75a647f/hypothesis-6.167.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b44839487832addbc248998dcbef103237b03d2c59b5b10597139a784a8b69ca", size = 1117911, upload-time = "2026-08-30T02:05:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2e/cf696d3655e0a6ba6a29d4d63d5bbce2216e631db712bd9efdd79a17f949/hypothesis-6.167.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc84771e9f0d93c1bebf2ae8729bd967053ea44985758351a21fe628c9413ae", size = 1163715, upload-time = "2026-08-30T02:04:47.768Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bb/d020d9e6493408a986524f70c6b698d50e98d671e82cc9e3b34203d4699e/hypothesis-6.167.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1db3d15a8f77c347c711bd64b844c4610e97e41d27fb3b2c96d9611f60f5d7f4", size = 1294056, upload-time = "2026-08-30T02:04:43.556Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d8/d1db4fa76eb885fbf33972569e74bf88023b1eb08771c768179ebd09d3e8/hypothesis-6.167.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f38a2fcdc318795f22de72155c9991114f3fa041c1cbbadd82297cb40ca81f1b", size = 1330254, upload-time = "2026-08-30T02:04:29.109Z" }, - { url = "https://files.pythonhosted.org/packages/aa/29/2351c5a6c500ccc22f48d8162b3cd769a6c9def2488f019b84ae7d7d349c/hypothesis-6.167.0-cp311-cp311-win_amd64.whl", hash = "sha256:2dae6eeddabd8f420dea09e203b1c5945639a9b240cf8f38d63e954d5fa3b219", size = 678349, upload-time = "2026-08-30T02:05:47.455Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4d/dbe4cccac3fac6c338403dd5da42408779a44ada86a4db9810e329d0c9cd/hypothesis-6.167.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:42e4e0a97dba87463a7636c370d2b8dff690a74896d20ec016c5b2eba72b50e5", size = 786972, upload-time = "2026-08-30T02:04:34.662Z" }, - { url = "https://files.pythonhosted.org/packages/df/56/35aee232af8b772f0a37c35219f4d0f0695870427f7a6302d61ab4cd6dbb/hypothesis-6.167.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aac94c4414be605e6a2e6b95e0d0a976f2c09a6397faac3da6b2f9b1a7207e08", size = 778539, upload-time = "2026-08-30T02:05:57.277Z" }, - { url = "https://files.pythonhosted.org/packages/c7/71/c4c02d17d5404556c21180377bd50684acd42163216ea16930d9e95b6752/hypothesis-6.167.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220e68920f0da423c78aa0e06ee8461ad2eb964ff3fdb7cafb78754f3a992411", size = 1116634, upload-time = "2026-08-30T02:04:57.254Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c4/769c8db3bb95cf1835e816032ed2bc0c37a6a6d339b671cecaaa121f5f53/hypothesis-6.167.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b6369c7dc98ed0889121f5013145d5dc621977d0f594a8db737b8986a7e14a5", size = 1162621, upload-time = "2026-08-30T02:05:29.273Z" }, - { url = "https://files.pythonhosted.org/packages/63/3a/aaead27f746803574a7e82c8d6e2cdb15917ada21b989e5dfcd1eaade92c/hypothesis-6.167.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc7da295b1692701ca190c0e99c190edaf10a69260b200a95edf220fe35c0ed2", size = 1292346, upload-time = "2026-08-30T02:04:39.026Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b0/108d89dfab22011edf80b536fb66056439e83c77d1bb2e7513bf0128d55a/hypothesis-6.167.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2402fd4f170a4023d3ba5a20858a39e9e8fac105eced4eb619d7d11271be6f6b", size = 1329063, upload-time = "2026-08-30T02:05:20.655Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fc/c312388154cf737ce7416201339dc8324c9c6b0726927e3581b57d426739/hypothesis-6.167.0-cp312-cp312-win_amd64.whl", hash = "sha256:004ddebe0f85bc96f63b570659d0d37ff5ad4f4be7fa4b98f3313ce405019610", size = 675887, upload-time = "2026-08-30T02:04:55.746Z" }, - { url = "https://files.pythonhosted.org/packages/94/1c/cf6854e55e7d35aa84de1c65a7f8bd8618dd15a8fea0c978a43c702852a1/hypothesis-6.167.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a4841ec07f0af94633e7f38b875cd472232c315cb58f2dc08f927e00084b159c", size = 786867, upload-time = "2026-08-30T02:04:14.94Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9d/6a5d6e744428d37bef067fd4d7767593bdd26a267c4c4103d7906dc9bfda/hypothesis-6.167.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07ea389b2351b1119142b2e736c5eb96aae361f988db3e1c36488c294b41552c", size = 778499, upload-time = "2026-08-30T02:05:13.474Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8c/beee8a3c429104e3ac7ccec5e0dc6d0cd0c70009fcf1a887c01ed47a4f73/hypothesis-6.167.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f060535e56a16029f7b3ef98da5a1a1e0816e76d9b4940c8b6237142d93401ed", size = 1116630, upload-time = "2026-08-30T02:04:27.847Z" }, - { url = "https://files.pythonhosted.org/packages/66/a7/ef7c79380525666204d77c8e8cf40be066acb04a1de56b838c0ec2e77e51/hypothesis-6.167.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfbfa3b76052270fb5b021c172506b7c72db3cabb1b57f96490f49e14006822", size = 1162486, upload-time = "2026-08-30T02:06:07.941Z" }, - { url = "https://files.pythonhosted.org/packages/f6/33/e017f20aec2a74e3d2032d8606c9b0e9729ee8c2fd580c4401f19216293e/hypothesis-6.167.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8789069253ef2e9091f1b3dbf364a658177de2711eac86626fa07851a37b7d4e", size = 1292150, upload-time = "2026-08-30T02:04:36.093Z" }, - { url = "https://files.pythonhosted.org/packages/32/4a/eee8b0a99646ac19a619fe8ac99befe3fbef610836345c4c9ac62e903249/hypothesis-6.167.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:56e0073e8bac881ae7d7e3f6bed36dd766cc3941b9c2ec76dfdf5ae77cc6acf2", size = 1328888, upload-time = "2026-08-30T02:04:31.698Z" }, - { url = "https://files.pythonhosted.org/packages/01/f9/a52e0ff204fc5e9296bdab9eb7b27881ed7cbd53830aad22de35f6ba5dcc/hypothesis-6.167.0-cp313-cp313-win_amd64.whl", hash = "sha256:342576693423821587babc15064c4f0f13630624a5523db1963be9109686ef09", size = 675845, upload-time = "2026-08-30T02:05:00.271Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6a/53246d6641937c60b111be801e1d61867fe8f5c2fa6dce4da682f3c28f5a/hypothesis-6.167.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d9a01908ee8820800bd20b4ff289654650f4058583d3a80e58bae6c293ce78ca", size = 786974, upload-time = "2026-08-30T02:05:10.06Z" }, - { url = "https://files.pythonhosted.org/packages/d0/38/41a954266db4ede5632131488d0c571c062e5776f62032a88e5ac0872724/hypothesis-6.167.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9a5cad77635f77494b65b4869dcf0b6398dd0eececfe31a6868c8bf4a5b5bf28", size = 778645, upload-time = "2026-08-30T02:04:13.631Z" }, - { url = "https://files.pythonhosted.org/packages/f6/c4/be221a5ac3c0cddd6a95dc20328dafe081fb1c97f45384960ff678e3778e/hypothesis-6.167.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c07b1cdf065c8a62e209951d26d8ce21f14e3b998755f1422c6e529dfd940e3f", size = 1116940, upload-time = "2026-08-30T02:05:36.337Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/af8a1a71f0c8517a957184b4a4af1450bbc307da17349d2762c06ca94dae/hypothesis-6.167.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44ae0dc7cf9b0a20d39da08ec1fbe2c31c9811781582a4b7aea5bb9dd4509cef", size = 1162669, upload-time = "2026-08-30T02:05:55.178Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/c0ce1230f11f75408522dac6b2cac5cc942b9805838dc33339a61d316dd8/hypothesis-6.167.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b9542a2199016998600091ea4f7bf5239f368c7c965fd335abed87b2c4bd1de", size = 1292718, upload-time = "2026-08-30T02:05:05.312Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/7544ad88f2faf9f45ef13889b94dadcd1e4a0d97b5558e535330f1afc65b/hypothesis-6.167.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bdbef6d0b2e7ee36192a5e457ff0258d221ffd807bc1e10e9a25dae0dd06dcbf", size = 1329107, upload-time = "2026-08-30T02:05:24.103Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a83af13ba023c31e16ea328c8ddee2c241c1a680052ade3b191bbb9287f3/hypothesis-6.167.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:c628536f8a1b29ade49668388afd5978da8fbbd678bfe960e297525d853c3707", size = 617997, upload-time = "2026-08-30T02:05:22.193Z" }, - { url = "https://files.pythonhosted.org/packages/df/05/560dbd7b95146add3d826d80d556ac8fc35ea01b4190f16795ba00f26f6c/hypothesis-6.167.0-cp314-cp314-win_amd64.whl", hash = "sha256:21039a571752c8faa69733b94f4fe04250a1dff759a87089bc3b423c1d216b4d", size = 675721, upload-time = "2026-08-30T02:05:49.26Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/7d2e3e67ab0862c978b5c8a591ae3dbae9eb1e8c1f625375e6beb513ec24/hypothesis-6.167.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:902f42b1f3c438278281266ba7f0c55202345287e8065e2eb98fabdcda8939eb", size = 785446, upload-time = "2026-08-30T02:06:21.091Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/73706a7291e03d798d686b9422c83fdf66fe955d35313143156dd05c3297/hypothesis-6.167.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3a235e3ef76f73e32922383bd507970cbc01fc08f17043d18a72508957af8f06", size = 777063, upload-time = "2026-08-30T02:05:06.954Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e8/ef7a3036abba07c3630cc60a8e7aa7ac619daa8d7dbb8e2689f4bde76dc7/hypothesis-6.167.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601965116dce06c1052ca0c89b69f0f1a49ad5b7d2da2b8352351c23296bd607", size = 1115266, upload-time = "2026-08-30T02:04:42.243Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/943686f9dfdd7ac6423dc998f9dbf055a656e13046d5ce0f99899a0ce72c/hypothesis-6.167.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578e5cefa5a293158342e3bbabc71e426ec29d5cdb41635789b55f4b48eb5f9e", size = 1161171, upload-time = "2026-08-30T02:04:24.068Z" }, - { url = "https://files.pythonhosted.org/packages/aa/96/5303010ae09a6d0480270145fc344ec0d331622132af3d2726b582a111a1/hypothesis-6.167.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7356b0c2cd1d0978b66c1e4b604a4a1221695b6396f4fd6f302a817ab0a09e59", size = 1290878, upload-time = "2026-08-30T02:05:59.211Z" }, - { url = "https://files.pythonhosted.org/packages/00/f4/cfedcb75ecc35349ed77d7acbc7b0c9b68c6f3a406a5b2993c7d35ec5f6c/hypothesis-6.167.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a287d1cc06c4591ac2369997e400c99d31ad2debe52cc0f9bff12f216f80fa8", size = 1327922, upload-time = "2026-08-30T02:04:22.559Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/36bd9dd73e035c4ec92535ab77753beab20b52cdf1f9ca5873bc686ff2f9/hypothesis-6.167.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b2846cac259c96b81c6539f722c3b9fd2458a85e6273b7986dc08ee05c92318", size = 675646, upload-time = "2026-08-30T02:04:20.12Z" }, - { url = "https://files.pythonhosted.org/packages/43/59/faa4ae00641d8b61475f1979b9cbdaeb3da321057f79120985529daa4afb/hypothesis-6.167.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:9a6896bf96592cadb3c268be95e47be8d2d093d3d9c3785fab77c13fc9b89143", size = 785024, upload-time = "2026-08-30T02:06:10.197Z" }, - { url = "https://files.pythonhosted.org/packages/f1/31/6383c81208c772090314c38ff29d798a3e4f864d19a2ac88025c629d77ad/hypothesis-6.167.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:cb75ab6af7c6e84717a718d208e8e59054c0eaa06cb290d8ec440a8b99d9fad4", size = 776692, upload-time = "2026-08-30T02:05:08.413Z" }, - { url = "https://files.pythonhosted.org/packages/97/68/92665f9361c33a29996917725118303de85b4283ed4da63ea24428310e4a/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc721a2fc6511806b680b0cc618de9a29385c25125544065a0c11c11095629d6", size = 1115035, upload-time = "2026-08-30T02:04:18.619Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/82fddbfa3953ab65f95ecbb4ff8155176b5100ebfe53a129fee6f23cc4e6/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7117613a8b62d32cda04be4ee31ef8482bfa9d2ba31107f878b38f9cd4e440d", size = 1137728, upload-time = "2026-08-30T02:04:37.506Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/b1da21d3ecf8b7155ba5fcfd7d98e644801dc35b70b30b97a0b3d74e916f/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dee0107bb4a80dcd040f037c86f7fc96b03a70e8cf3e003761904ecb85db0b40", size = 1140371, upload-time = "2026-08-30T02:06:01.885Z" }, - { url = "https://files.pythonhosted.org/packages/ee/69/00325c56f3e9020c4383e54c379c977fafceb2395c8d0f33e008b5c0b2af/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7852a0469cba37e777c01cf6601146a48f76c8407c33e4ee093c5502aa571685", size = 1160531, upload-time = "2026-08-30T02:06:12.298Z" }, - { url = "https://files.pythonhosted.org/packages/84/10/e60eee7abdd435947c1d41f1e4f5b2e0096102ef11e1cfbad3ccd3037a53/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:b8730e59b9e0439e4cd40d6ef47c61fb5b9d09185ea56e9a91573b1cb0e1ccf9", size = 1120523, upload-time = "2026-08-30T02:05:16.953Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/6aff7cba6e3cc134ed1f911675bc775070584f6b4ae8e6e6c739cd5d68c0/hypothesis-6.167.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eea70c11aab935580afab3247e3a54c7eeceaf80d8452cf33e2a38015c2c139e", size = 1149342, upload-time = "2026-08-30T02:04:17.444Z" }, - { url = "https://files.pythonhosted.org/packages/72/ed/4d778a057d66fd121cc457d698c8f77690becf806736a0ce01106f5667af/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:7973858cf8d8546322efca0ecf3728f55495641da48fd70fd9e8d47fb4fa4935", size = 1290338, upload-time = "2026-08-30T02:06:16.628Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/3f90361451c3d4cab6b74aaefbe0c60aa1da40210e49152c40677a379adf/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:91dbd4d673d696075b4aa9506b0c94421756364edb46e38f63ab5a0fa33dd3e1", size = 1415663, upload-time = "2026-08-30T02:05:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/3b/01/2925ee0324e36cce6ca8449f52a371458a160ba18cfcafa3bb13b2ae8f72/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa55764b18f139e126d3360e6227d88ca25cfd110573f6051bab1235308098b1", size = 1272067, upload-time = "2026-08-30T02:04:52.395Z" }, - { url = "https://files.pythonhosted.org/packages/e5/49/e0ba54f0f64d4f4dd680d90033408ced3edfeb6c639d9665a2f9d342a546/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:534430c0dd66bee5d4f9263a34ee2877912e796e0ff2827b98d91837e071dd84", size = 1291474, upload-time = "2026-08-30T02:04:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/8b/8e/46721d5228aef6e646ef62502aec263c6776bbca7b93a705ff83ab6789ba/hypothesis-6.167.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d0a82542a44bc192139c4d4e50800adf6e91b6629dc15ddd43ab870fe71a3654", size = 1327640, upload-time = "2026-08-30T02:04:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/52/84/3fc3aad8bb9d891394bd0a4d0a4c5774a3d652ebb6e4a0b5533eef0c437f/hypothesis-6.167.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:29f6fa02f7e98ca1ea2b67083d3267a6de443a513694d5d30214aa62fc2c13cf", size = 669042, upload-time = "2026-08-30T02:05:38.093Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a6/ffaa4b788cee5ec624649e10640c1c9f6b9f60501fa67e58553ccd3a4683/hypothesis-6.167.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:3532de0fef4cb383e5959aca1b9d2af66804f661cac855e328bb8b00992e8800", size = 675350, upload-time = "2026-08-30T02:04:44.922Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/b0733d76531186c8726c73ef187717f995c6b191d9bbf9ba6b28c7a87946/hypothesis-6.167.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:064c6341919abb5776e6b51f701858670b584d1963923b336e78e6e1edf2152d", size = 673044, upload-time = "2026-08-30T02:05:39.834Z" }, - { url = "https://files.pythonhosted.org/packages/68/78/c67b8791234a007073d89563f54b19d7d13ba5dc68ec0927cb3a33932ef7/hypothesis-6.167.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b121253ff03e89656cab6e8ceea51c08215f56bc58c5d9eea35d250a885300c5", size = 786782, upload-time = "2026-08-30T02:05:32.824Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/66f968d8dbd781dbcb7ddd50af1565a2b7df3a17eed3056741ab76d7484c/hypothesis-6.167.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d717181cbc0daeb673a64b421276f21d1d619deb00877cc383601ac6dce9571", size = 782631, upload-time = "2026-08-30T02:06:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7c/4e79e57a200fc0ebd4c98673f11e4125842218b3d8de2fa88e00189424a2/hypothesis-6.167.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0e57b70dbd8b6a1fdd3791614104f7565a48928d6edde0a6c819f9778327fd", size = 1118596, upload-time = "2026-08-30T02:04:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c7/ded7f4975ccfcddb4b2b2ffca243de79d1abe8632ff22301d9c6ad819f74/hypothesis-6.167.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b156573357689e2056030383d9012408c898dbcf3b245e8c639e559494d6865", size = 1165329, upload-time = "2026-08-30T02:06:25.627Z" }, - { url = "https://files.pythonhosted.org/packages/7a/85/22a996622913321687cceef7e76741ee4f49d4e78faf2c2c9bb8f6d12ca6/hypothesis-6.167.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d02f1c7901c81009a8f24f03d717d9ef1d2ba2012e1b912a9a4f142ca428a40a", size = 679294, upload-time = "2026-08-30T02:04:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/53/4d/3592ca336deafbd3e9b0f47dc4c727aa32d30e765ef6370da8ecd590d388/hypothesis-6.167.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d28118fd70e4e15ff9c308a98b312b544b6145ae45aaa3b566328c1fdee8058f", size = 785476, upload-time = "2026-08-30T19:51:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/18/bf/e33c431148994cbcb3332c6df94b833ecfb4aa6a8e51ea4b83da55ddd581/hypothesis-6.167.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e517be7f82a0a917758cc489a88b826b5371f56381fd94b4a8a09ce82d8de406", size = 781033, upload-time = "2026-08-30T19:51:27.314Z" }, + { url = "https://files.pythonhosted.org/packages/94/a3/e0de9a82c7e790a1def0801076e0ef43110f98e95ed54a3554877d0cb66d/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f8cec74c4fad7aeb0852cb34c2134b16db05d878ad3946a53337dace7016f4", size = 1117814, upload-time = "2026-08-30T19:53:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/71/a4/8dd6bdc909324d1c39da1c86d65f75512ae049c159952af4cfe8feb5f8d4/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd202d02d129197a5e771f8a11c7d30559927284c23ec3a8bd4f37a7955964d1", size = 1141639, upload-time = "2026-08-30T19:51:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bd/c13ed6145c360d0770415efd7d5a7e63c29905aeef52ab88004fe7e7f924/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b1e393ab01b71f683ba2a783785871cc6b81a6e41017780c64a5bc0b99759ae", size = 1143334, upload-time = "2026-08-30T19:50:38.045Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/060d79ed8504b54ced9ad16f33d674b1b98a9debe9733c02709d7dd5c71c/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c385c7741893404306f9e5559ab3835432e85e7c153e25f854c502c410bbcbb", size = 1163345, upload-time = "2026-08-30T19:53:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/6e68e2b705f729a6f7b4f41030022b1a5264c5434d3bcd917233d6801c6a/hypothesis-6.167.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:94920ca1fae70c26b0bd3fabbeef9437ffc17a39fe85696fb9a86187d92f6dba", size = 1123029, upload-time = "2026-08-30T19:52:03.134Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c0/d274fe37ed5ecd5ad8ed555edc1f5e2abc8e1c3be3d5404b7edd5cc353a8/hypothesis-6.167.1-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8d90ded2ffdc7e56b5e571993f384b52fade0a7b424e614f999cc2491789970", size = 1154003, upload-time = "2026-08-30T19:50:55.053Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2f/2b5bb386f43fc965eb86fd69fcb2bd62c08cb6d7c6708a40dc39b3b97440/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:40cd5de7dd252942a08480639f5850594b1aca4a463e8a7f15e1fb6c2c3760c1", size = 1293729, upload-time = "2026-08-30T19:52:59.481Z" }, + { url = "https://files.pythonhosted.org/packages/ac/32/22436b072d79011fe81abb933edcd2476057c7b971588c5f3caf07519a88/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b9c33f921ddc7fea93660eca408b25fe755516e22ec7ab21cb9951031f1cd608", size = 1419248, upload-time = "2026-08-30T19:50:52.903Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/abf39faaff0f78112112a82316a5c9fe472574480c1ecee526734775b812/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:495989cf0a5ee03f7f9598ee9efeaabf15fd861ec52b5a9d6435849453e17e5d", size = 1274903, upload-time = "2026-08-30T19:52:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/69b89ed5692ba3aad117facfb9ce099633a22c35acc3d64829b72253ec8c/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:bc73c46ce8ff93b0eb220f2b75adcbd9fcc9112078a74d3522d2532ad8069bad", size = 1294185, upload-time = "2026-08-30T19:50:35.038Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1e/55dfcbe45c72df0a5c5b86a6b7c9365121acab69c2cc060bd55336a48c8f/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36e83e1d7e97aaacbf6cd778e14a841344f848a674b20dfe4fe997546a6a2151", size = 1330013, upload-time = "2026-08-30T19:52:54.841Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a6/0a36cead4ccff58bedb5d1aa2f894f7a580c317424b4fa788c6b22232b2d/hypothesis-6.167.1-cp310-abi3-win32.whl", hash = "sha256:fb4d87454d2459c2ccb541a4c61c92ce13058b91305ed3304695a409a1d886e4", size = 671942, upload-time = "2026-08-30T19:51:32.732Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5b/360285ed42109f5ef48d98ca9ffcf71d1477130c0ff1f1a8d535c8507259/hypothesis-6.167.1-cp310-abi3-win_amd64.whl", hash = "sha256:5e35f98b427bf438a946203426b485dd5b62485f3d5a69a0e0862870a545e518", size = 678637, upload-time = "2026-08-30T19:50:33.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/2d/cc084c1a8bfa296048ec0461f0fa11731abe0097f5c2310c8d39d94c8dc4/hypothesis-6.167.1-cp310-abi3-win_arm64.whl", hash = "sha256:dd6a0808a2eb8b5b1ac06bca4244eee18ed2c0e7b105599e1662203d164317b5", size = 676657, upload-time = "2026-08-30T19:51:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8e/e0f470823bc301a97a8e6156806f6322f5ab99a2f40b6c604261966b3393/hypothesis-6.167.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:64cf8b7ac9a0cc80dad8884f81a2e50f0b79956694927d40e21e0cfa48830b8a", size = 786180, upload-time = "2026-08-30T19:52:09.714Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4d/6e9e430ded5e226f245cd7e55923c7675311fa2115bc4262b5802edfcbb2/hypothesis-6.167.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69b92f037080cefb949c5f9683873abac12283e0dc77201df089369c1ef67e4c", size = 781901, upload-time = "2026-08-30T19:50:42.641Z" }, + { url = "https://files.pythonhosted.org/packages/bd/aa/8df2711daf3ace849045482492b7f178fb55b79201f332477f6eef590b46/hypothesis-6.167.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b334180260de636a0b3017dd96d63709da0c4182c9cb28f52dd6cda78dc933", size = 1118135, upload-time = "2026-08-30T19:51:54.599Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4f/6040b58ffc511013394ba027191dd7e2886c5ae39e0b7872ab15503803fe/hypothesis-6.167.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:769d0242531067e6daf16b6c9894fd9584139884de887023328e3c5658993597", size = 1163947, upload-time = "2026-08-30T19:50:45.908Z" }, + { url = "https://files.pythonhosted.org/packages/ca/66/24aedbae1b56e71308d0aef4415e37c6bb22068c77e95337cdf9d74cd8b1/hypothesis-6.167.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:22b6c3def5446016148523b74ef9da4948bec5ef05865d56c99ba187f4092663", size = 1294290, upload-time = "2026-08-30T19:52:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/a063e3ab97851f4425918f0fcd407d0f426184c26b09f3bda7369e3d38d7/hypothesis-6.167.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbbb6bc17a5a04120a5bfd830335b8b301a22d21d2262804be330c235031b4c5", size = 1330694, upload-time = "2026-08-30T19:51:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f6/6ebb84d532c0d5b36b3bab7b477a167685b2210a0246c7101557bfa4751c/hypothesis-6.167.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdc7e20161f21f14c2d7a057054521db0a8c4bbb647d2e50c90c84f28e4621a0", size = 678586, upload-time = "2026-08-30T19:52:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/14/2445b7b1a0c8db61c6812b74606ed7a4f41e3ea0e0c103313e25995b82d5/hypothesis-6.167.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:613bf10e6e490daaa88eb4bf06fb3aacf6572b887e2f9fa5d0bac1be96a18c00", size = 785945, upload-time = "2026-08-30T19:51:19.221Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f6/67926d308a9ab19bb7dfb5118832fa74b508f94871fadbb3370629decbfc/hypothesis-6.167.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f715d912560dd4df3daab4c1fd98c01132eea7ab292f5b9e1d28fc419fe63348", size = 781726, upload-time = "2026-08-30T19:52:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/de/22ac0e272530b36ad840170bf661ff89df8648052bb6a37dba520b312f1f/hypothesis-6.167.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdd2fc0232b47910e9621f8c1b5732381438055b03fcc69180e1ef3659b7e70", size = 1117929, upload-time = "2026-08-30T19:51:58.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1f/b72b51bac9a7d330bcdda01dc0ab1abe76e1c29ff522effd2d1be4e23702/hypothesis-6.167.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:163e4cebd4b2380ff92f5d36bd04697e82b13973794440694da768521e1e2eab", size = 1163855, upload-time = "2026-08-30T19:52:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b4/02424328951f0244f7dd3f620775ed22d39dc234e92759f9d2980150252b/hypothesis-6.167.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de4e67289f86e358732eb16b345f9d1f987c33e44b10d4ffa3ccd715dea9316", size = 1294177, upload-time = "2026-08-30T19:50:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/d8/09/2262d6b362c81066ad451fda48633b2d3ea6cf2d0d673ee6554ad8f86c58/hypothesis-6.167.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fcfd20792f62d65729f850ea50328c1f8b09874d5960b122715b9e6784a7a547", size = 1330267, upload-time = "2026-08-30T19:51:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/19e8c02eebfe89592dfd12372032e8868ad40e6d40b38e5ecc4935ce194f/hypothesis-6.167.1-cp311-cp311-win_amd64.whl", hash = "sha256:ebb841d21156039d7da0a41fa9de4ccf468510a4e4d8144f4fe2b3f31239ef3b", size = 678401, upload-time = "2026-08-30T19:51:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/72/82/07987292cfb59c73ce6574e2912c015f678d5aa4d8c0712b78ae4415a535/hypothesis-6.167.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1937ae4e23f7dde6d4202d3d08c2633bcd535a091bdf866b8799abaabcb1e6f0", size = 787050, upload-time = "2026-08-30T19:51:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e9/0d37051bec44ec433d87da03c9a7fe389b1b58210790c1f166af249bf941/hypothesis-6.167.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1434bbd25d05aaf75c4e6829b4cad8e9931b690f840d92b747b2e6e5af575922", size = 778617, upload-time = "2026-08-30T19:50:31.939Z" }, + { url = "https://files.pythonhosted.org/packages/13/2a/60c18a493215c22c9cfcb4574b381497bd1971eed9fb5f9831b26e73cffb/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3c09428e553b1dd2f9abbc4738377c58bf6d74cb0b8b528cc1dde3a9cdfbe8", size = 1116743, upload-time = "2026-08-30T19:50:49.293Z" }, + { url = "https://files.pythonhosted.org/packages/12/1f/b6796f11d6502e0b1764aec99f2792ca60382b6a45e26bbe163dc5757980/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04f807b85d425a7005e8a24498ca832bf5590f0d306737471d94c842569cecef", size = 1162718, upload-time = "2026-08-30T19:52:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/e3cf35474b799e299fa08980b6756f500d730781686916ab65f87cbc0613/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:803c6a98ff66cee4caf03245bfd00e442a907264031b994a3a650dc6e4786f51", size = 1292417, upload-time = "2026-08-30T19:50:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/875803c80c373a1628f8eb62f215ad23ce7b50ed61f884d6be0838ebea4a/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21a7122ddf072906083e3704fe3961ecbc49d7d30a9b51bcd525d977b3afe65e", size = 1329035, upload-time = "2026-08-30T19:51:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/a8daed3796623884471dc0ee8ed63917b1e2b979b4074bcea19a964fcd71/hypothesis-6.167.1-cp312-cp312-win_amd64.whl", hash = "sha256:a2837c60d782eb0b8a910c541264675b9d11486e186af8c82a5e2920b5fe4fe8", size = 675966, upload-time = "2026-08-30T19:51:56.58Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/dca7b211804f60c789aced2792b1e7803ccd8b70b79041cbb92788df5d19/hypothesis-6.167.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6478d19a7887731cc2afaa1ec15f62811c9ceb6fd18e5b7563e0a18399a9528f", size = 786947, upload-time = "2026-08-30T19:51:29.165Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6a/3cffa138492c9e3d5f98f4ff8b467273dc87af6ca3c18084272d106bde10/hypothesis-6.167.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f13167a4b81c93e7e051d1f02790814a6495fb79cacf3fb89560a796a2f7d00", size = 778584, upload-time = "2026-08-30T19:52:25.091Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7d/e8039791aaca3b21557bc520a71cdb88751892f66fd1a0a459b59872e463/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef7dd225f7df7d74d1c5a905592cd8b4cd348e6be639b189a43def8b0b5dd79", size = 1116749, upload-time = "2026-08-30T19:52:38.178Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/f9b8d93bd6178870f0daa868ca99915f6d9df1f99dc7291e9ce2743a6dc5/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d7585429f2263d3ceeb3474bae3871024630a7a598e71eeb4b0dcf03e291623", size = 1162599, upload-time = "2026-08-30T19:52:11.72Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/53d419094e6f8a7e7377c09de15ac23f842ab698ff07241f7b73e19bd559/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fb9194f450417cf35f66b6c72737cc6b8f21f20567ba4d39822c64f0d1075784", size = 1292230, upload-time = "2026-08-30T19:52:49.732Z" }, + { url = "https://files.pythonhosted.org/packages/5c/df/cf4c482323ae4f06b5326b5bdd89cf17d8232fdb3186c9913e0b19a5fa58/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c63a0d292a5dde3c0fe999892e76d8375a003ca40c0c00d763f3360f91be5b96", size = 1328899, upload-time = "2026-08-30T19:51:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/49/05/780c4b0396491d294fda69a541cb1dedb37fb9eb2e3a696e85fe19064c40/hypothesis-6.167.1-cp313-cp313-win_amd64.whl", hash = "sha256:ff07f98a0b230632bb2836b5dad3e94d85c114ae155a316afd251c58760958ae", size = 675927, upload-time = "2026-08-30T19:50:58.472Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f1/1e602f090dcb7e38655f1f7909482742891332275fc01f241e255cdfa514/hypothesis-6.167.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:fcfc2a78fc1025644f889a74684b3201f4652ce8e6694c2a01af0f100d0348cf", size = 787054, upload-time = "2026-08-30T19:50:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/cfa930719c7af5a33627abba826a3fa2efa61a5f23e38d4111eace5dfe53/hypothesis-6.167.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:769bdd9aa0af08c063327730ab6dc18b7a23837a2912f2aeaab3912f11a7e3ad", size = 778721, upload-time = "2026-08-30T19:50:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/4c4bc3d319eac85bfe17515c9786bf49e57181ca8757886110d2cfb13d10/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d75d44bdead6679b6ee9a7c90d10207db865ca0c77c5212103b5ff421379f99e", size = 1116972, upload-time = "2026-08-30T19:51:52.472Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/0d61ceef2739e7b96ea1faa0f3d5aa5917c8156797993bf3acbadfcd7f0a/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742be00d7bb53d10634e6435e5b98f51fcdbe7ed377d473ab7387d9499c87169", size = 1162776, upload-time = "2026-08-30T19:51:09.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cf/756666ce2262e90fd61fec41a95548cceab94b0669381d8f0387cd89af93/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b6fcdc8d03b37a902262be13112d113bb4ac87edf3b08afb47f3d1210deb038a", size = 1292748, upload-time = "2026-08-30T19:51:17.22Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b4/87eb3c695d6c37fb44f4d49f9faa2033af496e24965658942a1706e22620/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56e7841514276c308c2bb4d033cf01860d0fc8c76e2b79ce748a9f123eaf83b", size = 1329101, upload-time = "2026-08-30T19:51:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3b/87faa4a86533eaaa19037741fb9cdde8647f7ffdf8fd4279828ac9d81f8b/hypothesis-6.167.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bbf4f0cad201d0b8e821e82ad828b2aec99ce6d9967779eecb2cad4d4a93debd", size = 618079, upload-time = "2026-08-30T19:52:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/e0/46/96b7ac9605887447d267b4b3a9ecf61c6caaabf39eef667173b0cc9222b3/hypothesis-6.167.1-cp314-cp314-win_amd64.whl", hash = "sha256:3e04f6001299708b6fd4512267b189c0b029ef1e34500deb4e4c9639023598d7", size = 675812, upload-time = "2026-08-30T19:52:52.298Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/0337a91c50ce4be323c3d6aa852fcf08199ffbb1072da09fbe6d602f4dfe/hypothesis-6.167.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:47c99256df28555ecc2aed0e22ca17cd61c63c8c44207a07b4e402cc49661fae", size = 785525, upload-time = "2026-08-30T19:51:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/5a/08/9bb52de855169d31888c7033ee2f94b94138fde021c1af9dbc7ba5e83cd5/hypothesis-6.167.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5d6614e88fd267bbd870e3ec02f8a5387897d2d573626a02f5ac06d81533afa6", size = 777142, upload-time = "2026-08-30T19:52:18.472Z" }, + { url = "https://files.pythonhosted.org/packages/85/79/f1a7e088e13a641357abb9b43d75c116c2a0902711b1a25a203864b96c9b/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27829aa89fe2e47c5c8d13b3ce31e0f53a01a98f76a4f99cfa3369ab35362f33", size = 1115311, upload-time = "2026-08-30T19:52:40.975Z" }, + { url = "https://files.pythonhosted.org/packages/e0/38/e28b1fc20bd3d67d43cf1aab7a15daa2a24ec01d17a153e82fcf38c882f3/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e936777d92ae27393b4a941839bbb43c1f339b5a0e394c7f3730454cdf091b3a", size = 1161238, upload-time = "2026-08-30T19:52:20.501Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/9b4fc7992166299e0fc5c13c8766919ae57d0fb9eed5319b7a3bad4f2f17/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:971ce0d8a367a37c4690b83a2e7f6ef832fa3543357eb6da0da27ba078e5088a", size = 1290974, upload-time = "2026-08-30T19:51:15.673Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/ca086eea02588212ab796ee4bd7fe6ed514e10d1a99967e478691608e8d9/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af84ce2416be2a65bc0ea18e64d2dbb9796b7692593b5b2064d60ea1d52ec1e2", size = 1327969, upload-time = "2026-08-30T19:52:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/1875380fa30e8411553e76b3e9695aca0845f9f265523f20f879bdea2b82/hypothesis-6.167.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3b596efec5bd714588e3bb269544d993c5258c979f3a26f51fadf62c215d0e68", size = 675735, upload-time = "2026-08-30T19:52:22.821Z" }, + { url = "https://files.pythonhosted.org/packages/b0/45/59abecd75e52b9dfb5b3eb991276f54954c44917a1c83d148cfb3580bd39/hypothesis-6.167.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:c25c556d51d55d94988dc0a2c716d471ff19cf9632cd33f5e6db2914d802428a", size = 785097, upload-time = "2026-08-30T19:51:12.528Z" }, + { url = "https://files.pythonhosted.org/packages/01/7c/e6d978dc9564ba70352da60c00f55f6ad7d66d99ecbf336a228978206cb4/hypothesis-6.167.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b57e950f9d5c93ca335bc612e8fa8fb49abb187c3fc9d5e7d9966d52eb27d747", size = 776798, upload-time = "2026-08-30T19:50:47.247Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a4/f8ecedcf96790aab69d750afe3fcbf503229d0bb4e0c32be655385a4fc8c/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0807ae8d399162827fc1c396ab4c697a41921c2a2baacfada439771e9dc2b867", size = 1115116, upload-time = "2026-08-30T19:52:16.029Z" }, + { url = "https://files.pythonhosted.org/packages/7e/97/8bca7c262ac4fcb1ee684c04e4ba75f26541d3a30417e3743d19912d257e/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:546fef39c7aadba74bf3e592585694a71340d1775e9b3274bb3f94106dbde4b7", size = 1137812, upload-time = "2026-08-30T19:51:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/9cb4a7fc2aa2b2ad063b446c378f0d7acfa5303e84afd1b1374ba23fd6f3/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17a5618b6a5b84f17c8acb3bce37122647cf7a3e48b660a39d68a773bd627dd", size = 1140384, upload-time = "2026-08-30T19:52:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/813bf7efa18f11ce938a0da22a7518a54db46d4a181ebf4cb0a8061c263f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff5ad833480c1e34ae902cb52fc00802b08ac6c87bd22d7e5f04fd925869608", size = 1160569, upload-time = "2026-08-30T19:51:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/1d/6658d9294ed33bb17da4acf06fe63b010b205ea1861f6921c38060793255/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:630eb37df80b5bc4ec6f391b13caaecc06942ff5da3aadebacf85f53bbc55757", size = 1120605, upload-time = "2026-08-30T19:53:01.848Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/a75821c0e879223a2635b8eded84ae874cb6c711b23e9930668008d0b13f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc4e65f7c43b187f7a40964706b5ded1073e0c1839e9fb5e041d7ed973bb65fe", size = 1149479, upload-time = "2026-08-30T19:51:30.972Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f2/c8c3faf4ec796d6dbf36b84662806696434aef38616e5a65b46188c04262/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e849f518cbc4e76ab15f2f1473c60dd3103da8d32399187325ceb84309105976", size = 1290423, upload-time = "2026-08-30T19:50:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/e0903abe7e8634cedb5414931452e82daacdd3b8b46d6348bbefcaa45f2a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:5c5a26d4d3dca0c84e01bde41df4cabaa5a373c7393f9eef372d19fe93b07ccd", size = 1415749, upload-time = "2026-08-30T19:51:48.456Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/03f09c1ecac1dfb0f4cd7fcc6dc50d9c6ea8067b295a728e242650bafe32/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4819adbc5911648f6bfaeb574f276add184b4b49f54731dbde46fd71256bb157", size = 1272086, upload-time = "2026-08-30T19:52:29.368Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/7a63bfc0bfbaf000f71352c4faac72ff611376330a2ce2e9a1bf4668848a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:3ad7206de9c398c8da5745b69b5ba2ef45100082eeb174656490bc4f262b112c", size = 1291553, upload-time = "2026-08-30T19:52:07.545Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/8065bdab53bc81743ca68fc76ca53fc7531a5b3f01c0de4ba40467955d6a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:96d5e8017a9508f06c8a61a6130cb0d0b4810847ed5c76923cb5cfb9952b31af", size = 1327734, upload-time = "2026-08-30T19:51:42.306Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a0/5c15d480aea3a8e6e5c17c7cb1170171707ac643ffd319473bc194743ad8/hypothesis-6.167.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:a4e4de36a397cba49d949d89cbc26135977c15f9d797caa95317962ceb5b5674", size = 669115, upload-time = "2026-08-30T19:51:14.192Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/4cf494bc96384189fedb7d3f272580315f2284a9f8a7f6a59796612eb76d/hypothesis-6.167.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:f6fe9c40ab14def363d9e7ab22863fa31652bd5e08f8495b34ff7bd0062b3f8d", size = 675438, upload-time = "2026-08-30T19:51:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/b0/7f/db1a37e5f45be32c0e64f9ed1268eba56aeedcb2ef20d195fa60c6610347/hypothesis-6.167.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:627ce3bd166799a6c0ddcf1351049be5b9a772d5bce436216d42b41a935f42c0", size = 673123, upload-time = "2026-08-30T19:52:13.991Z" }, + { url = "https://files.pythonhosted.org/packages/53/bc/a77ee57eb8fb13f2b5bdfb4a1ea3f32713c50420f0208e84fbe590fad1ad/hypothesis-6.167.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:436027c9a00eb11a2ca3d608147ca2d0d623b4f02878c56a50fc3f3f58c2b41b", size = 786862, upload-time = "2026-08-30T19:51:38.287Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3f/cc9c9120fad719e683914b9204b38f1a30721bc06344f465fc36427cc45e/hypothesis-6.167.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:35e90c121b1518d7428a45e6b0d5c6d06e0ed9eaa567f1106e1f09dae006d6da", size = 782711, upload-time = "2026-08-30T19:50:28.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/a48824ba4ad1257904bde4654099851febf0b4c3f018c8174b33d4ba0308/hypothesis-6.167.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c903b4f1c8531736fc7e8e537f47ef509756b731a32a5e5e7014e5291343acb", size = 1118685, upload-time = "2026-08-30T19:52:01.045Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f3/c6bcfb38c4b4cd22494902f5368b5815425f03eeb5159741c7d910a69af5/hypothesis-6.167.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ca252991fdbe2ff5c611a1cc4d972d4e009eb45292c7802faa2190f995dc50", size = 1165389, upload-time = "2026-08-30T19:50:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f6/d1d19d115a4c0aa35c87b9e5d570b0a7c9f42f816087849cf29c58664426/hypothesis-6.167.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c91f6f2f15b8bc6e474b824f931247c39711231e7b1b2f68277726e0ae1c728", size = 679392, upload-time = "2026-08-30T19:51:00.264Z" }, ] [[package]] From 5172f0bbc04c06675f13c7222f651db811cd2305 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:13:21 +0200 Subject: [PATCH 050/322] chore: Bump @types/node from 26.4.0 to 26.4.1 in the all-npm group (#4954) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index dba2c13436..6f0fe74bda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.4.0", + "@types/node": "^26.4.1", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" diff --git a/package.json b/package.json index 6ed28a9f21..9949a65b30 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.4.0", + "@types/node": "^26.4.1", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", From a8d02402b8d4a9bcee47208723154eb80b5454ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:38:23 +0200 Subject: [PATCH 051/322] chore: Bump smol-toml from 1.7.0 to 1.7.2 (#4960) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f0fe74bda..3bae0e04a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2464,9 +2464,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", - "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.2.tgz", + "integrity": "sha512-pXFZ9B2WinEPzxWkMmlYE/oYx2BP+qLrE95wP8tCuK901uLSMGdCb6QSr82z+wnhXkG4+cO+OMLbZB2Cn+97zw==", "dev": true, "license": "BSD-3-Clause", "engines": { From 8d5544744e853625481120ac1fdfbecf62104b2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:48:09 +0200 Subject: [PATCH 052/322] chore: Bump the all-nuget group with 11 updates (#4959) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Directory.Build.props | 6 +++--- src/Fable.Analyzers/Fable.Analyzers.fsproj | 2 +- src/Fable.Build/Fable.Build.fsproj | 4 ++-- src/Fable.Cli/Fable.Cli.fsproj | 2 +- src/Fable.Compiler/Fable.Compiler.fsproj | 2 +- tests/Js/Main/Fable.Tests.fsproj | 2 +- tests/Python/Fable.Tests.Python.fsproj | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 79213107ea..dd7d9b9e20 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "fsharp-analyzers": { - "version": "0.37.2", + "version": "0.38.0", "commands": [ "fsharp-analyzers" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af703934c1..d9eff242ce 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,16 +23,16 @@ - + all build - + all analyzers - + all analyzers diff --git a/src/Fable.Analyzers/Fable.Analyzers.fsproj b/src/Fable.Analyzers/Fable.Analyzers.fsproj index 7e7ae1360d..81249e8a5a 100644 --- a/src/Fable.Analyzers/Fable.Analyzers.fsproj +++ b/src/Fable.Analyzers/Fable.Analyzers.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index eff3e2622f..469bde2bac 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -49,8 +49,8 @@ - - + + diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index ffb73f4a73..c5340e3a94 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -53,7 +53,7 @@ all - + diff --git a/src/Fable.Compiler/Fable.Compiler.fsproj b/src/Fable.Compiler/Fable.Compiler.fsproj index 2410db69d8..0e15e7a1c5 100644 --- a/src/Fable.Compiler/Fable.Compiler.fsproj +++ b/src/Fable.Compiler/Fable.Compiler.fsproj @@ -43,7 +43,7 @@ - + diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 933d6f4d4f..99d1843057 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -12,7 +12,7 @@ - + diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e2374f7e18..a86f7654c6 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -17,7 +17,7 @@ - + From f244c0ab6097d692d2e89e2bc563a50088c41c99 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 10 Sep 2026 23:42:37 +0200 Subject: [PATCH 053/322] ci(dart): fetch pub.dev packages anonymously (#4963) --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa82bf0420..fcd73438c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -286,6 +286,11 @@ jobs: - name: Setup Dart SDK uses: dart-lang/setup-dart@v1 + # setup-dart registers a pub.dev token, which pub then sends on dependency + # fetches too. This job only consumes packages, so drop it and stay anonymous. + - name: Use pub.dev anonymously + run: dart pub token remove https://pub.dev + - name: Fable Tests - Dart run: ./build.sh test dart From 94b59294b4749c1f871484b843604da81ceb71ad Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 10 Sep 2026 23:42:53 +0200 Subject: [PATCH 054/322] fix(python): support bytes() on generic and bool arrays (#4961) --- src/fable-library-py/src/array.rs | 13 ++++++--- src/fable-library-py/tests/test_array.py | 35 ++++++++++++++++++++++++ tests/Python/TestConvert.fs | 8 ++++++ tests/Python/TestGuid.fs | 10 +++++++ 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/fable-library-py/src/array.rs b/src/fable-library-py/src/array.rs index 4e7ff4e4d8..8d68d631f3 100644 --- a/src/fable-library-py/src/array.rs +++ b/src/fable-library-py/src/array.rs @@ -665,8 +665,6 @@ impl FSharpArray { /// For Int8/UInt8 arrays, the result is a direct byte representation. /// For larger types (Int16, Int32, Float64, etc.), each element occupies /// multiple bytes in native byte order. - /// - /// Returns NotImplemented for String and PyObject arrays. pub fn __bytes__(&self, py: Python<'_>) -> PyResult> { match &self.storage { // For UInt8/Int8 arrays, we can create bytes directly @@ -756,8 +754,15 @@ impl FSharpArray { }); Ok(bytes.into()) } - // For non-numeric types, return NotImplemented - _ => Ok(py.NotImplemented()), + NativeArray::PyObject(vec) => { + let list = PyList::new(py, vec.iter().map(|item| item.bind(py)))?; + Ok(py.get_type::().call1((list,))?.unbind()) + } + NativeArray::Bool(vec) => { + let bytes = + PyBytes::new(py, &vec.iter().map(|b| u8::from(*b)).collect::>()); + Ok(bytes.into()) + } } } diff --git a/src/fable-library-py/tests/test_array.py b/src/fable-library-py/tests/test_array.py index cefdcf7d79..15224ef478 100644 --- a/src/fable-library-py/tests/test_array.py +++ b/src/fable-library-py/tests/test_array.py @@ -1646,3 +1646,38 @@ def test_generic_copy_round_trip(elements: list[int]) -> None: if elements: copied[0] = "changed" assert list(arr) == list(elements) + + +def test_bytes_from_generic_array() -> None: + arr = Array(array_type="Generic", elements=[byte(1), byte(2), byte(255)]) + assert bytes(arr) == b"\x01\x02\xff" + + +def test_bytes_from_empty_generic_array() -> None: + assert bytes(Array(array_type="Generic", elements=[])) == b"" + + +@pytest.mark.parametrize( + "elements, error", + [ + (["a"], TypeError), + ([None], TypeError), + ([1.5], TypeError), + ([300], ValueError), + ([-1], ValueError), + ], +) +def test_bytes_from_generic_array_rejects_non_bytes(elements: list[Any], error: type[Exception]) -> None: + arr = Array(array_type="Generic", elements=elements) + with pytest.raises(error): + bytes(arr) + + +def test_bytes_from_bool_array() -> None: + arr = Array(array_type="Bool", elements=[True, False, True]) + assert bytes(arr) == b"\x01\x00\x01" + + +def test_bytes_from_bool_array_matches_generic_array() -> None: + elements = [True, False, True] + assert bytes(Array(array_type="Bool", elements=elements)) == bytes(Array(array_type="Generic", elements=elements)) diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 0ab2b74a89..95dba9c4cb 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -1056,6 +1056,14 @@ let ``test Convert.ToBase64String works`` () = Convert.ToBase64String(bytes) |> equal "AgQGCAoMDhASFA==" +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +[] +let ``test Convert.ToBase64String works on an array built by a generic function`` () = + let bytes = genericOfList [ 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy ] + Convert.ToBase64String(bytes) + |> equal "AgQGCAoMDhASFA==" + [] let ``test Convert.FromBase64String works`` () = Convert.FromBase64String("AgQGCAoMDhASFA==") diff --git a/tests/Python/TestGuid.fs b/tests/Python/TestGuid.fs index f58410474b..16cfc2ca5e 100644 --- a/tests/Python/TestGuid.fs +++ b/tests/Python/TestGuid.fs @@ -164,6 +164,16 @@ let ``test Convert byte array built from a list or seq to Guid works`` () = Guid(Array.ofList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" Guid(Array.ofSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" +let private genericOfList<'T> (xs: 'T list) : 'T[] = Array.ofList xs + +let private genericOfSeq<'T> (xs: 'T seq) : 'T[] = Array.ofSeq xs + +[] +let ``test Convert byte array built by a generic function to Guid works`` () = + let bytes = id [6uy; 128uy; 37uy; 150uy; 186uy; 196uy; 127uy; 74uy; 128uy; 196uy; 222uy; 127uy; 43uy; 40uy; 152uy; 197uy] + Guid(genericOfList bytes).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + Guid(genericOfSeq (Seq.ofList bytes)).ToString() |> equal "96258006-c4ba-4a7f-80c4-de7f2b2898c5" + // [] // let ``Guid.ToString works with formats`` () = // let g = Guid.Parse("96258006-c4ba-4a7f-80c4-de7f2b2898c5") From 91d80f185fb38e4d23fd9e906037a14daab3e3b9 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 10 Sep 2026 23:05:42 +0200 Subject: [PATCH 055/322] fix(js/ts): keep the sign of negative decimals below 0.1 --- src/fable-library-ts/lib/big.js | 19 ++++++++++++------- tests/Js/Main/ArithmeticTests.fs | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 22a6c5ba50..374d37c99a 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -267,6 +267,11 @@ function round(x, sd, rm, more) { } +function hasNonzeroDigit(x) { + return x.c.some(d => d !== 0); +} + + /* * Return a string representing the value of Big x in normal or exponential notation. * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. @@ -932,7 +937,7 @@ P.times = P.mul = function (y) { */ P.toExponential = function (dp, rm) { var x = this, - n = x.c[0]; + n = hasNonzeroDigit(x); if (dp !== UNDEFINED) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { @@ -942,7 +947,7 @@ P.toExponential = function (dp, rm) { for (; x.c.length < dp;) x.c.push(0); } - return stringify(x, true, !!n); + return stringify(x, true, n); }; @@ -958,7 +963,7 @@ P.toExponential = function (dp, rm) { */ P.toFixed = function (dp, rm) { var x = this, - n = x.c[0]; + n = hasNonzeroDigit(x); if (dp !== UNDEFINED) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { @@ -970,7 +975,7 @@ P.toFixed = function (dp, rm) { for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); } - return stringify(x, false, !!n); + return stringify(x, false, n); }; @@ -983,7 +988,7 @@ P.toFixed = function (dp, rm) { P.toJSON = P.toString = function () { var x = this, Big = x.constructor; - return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]); + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, hasNonzeroDigit(x)); }; @@ -1011,7 +1016,7 @@ P.toNumber = function () { P.toPrecision = function (sd, rm) { var x = this, Big = x.constructor, - n = x.c[0]; + n = hasNonzeroDigit(x); if (sd !== UNDEFINED) { if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { @@ -1021,7 +1026,7 @@ P.toPrecision = function (sd, rm) { for (; x.c.length < sd;) x.c.push(0); } - return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, !!n); + return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, n); }; diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 53bf938f92..ae655de116 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -195,6 +195,21 @@ let tests = testCase "Decimal division precision is kept" <| fun () -> string (8M / 3M) |> equal "2.6666666666666666666666666667" + testCase "Decimal ToString keeps the sign of values below 0.1" <| fun () -> + string (decimal -0.001) |> equal "-0.001" + string (decimal -0.05) |> equal "-0.05" + string (decimal -0.1) |> equal "-0.1" + string (decimal -1.5) |> equal "-1.5" + string -0.001M |> equal "-0.001" + string (decimal 0) |> equal "0" + + testCase "Decimal ToString round-trips negative values parsed from a string" <| fun () -> + string (decimal "-0.000001") |> equal "-0.000001" + string (decimal "-0.001") |> equal "-0.001" + string (decimal "-0.05") |> equal "-0.05" + string (decimal "-0.5") |> equal "-0.5" + string (decimal "-0.000") |> equal "0.000" + testCase "Decimal division works" <| fun () -> let a = decimal 0.00001 let b = 1000.M From 6348b35a44fa027ed9bab518b67f133063e1bcc2 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 10 Sep 2026 23:05:42 +0200 Subject: [PATCH 056/322] build: track fable-library-ts JS sources in build cache --- src/Fable.Build/FableLibrary/TypeScript.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Fable.Build/FableLibrary/TypeScript.fs b/src/Fable.Build/FableLibrary/TypeScript.fs index 6275e9b4a7..9d35b3527b 100644 --- a/src/Fable.Build/FableLibrary/TypeScript.fs +++ b/src/Fable.Build/FableLibrary/TypeScript.fs @@ -16,6 +16,7 @@ type BuildFableLibraryTypeScript() = [ Path.Combine("src", "fable-library-ts", "**", "*.ts") Path.Combine("src", "fable-library-ts", "**", "*.fs") + Path.Combine("src", "fable-library-ts", "**", "*.js") ], Path.Combine(".", "temp", "fable-library-ts") ) From 1c02557efd113d8d30a922c74c3fc9d1e7168cfd Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 10 Sep 2026 23:12:23 +0200 Subject: [PATCH 057/322] fix(js/ts): respect the precision when formatting decimals below 0.1 --- src/fable-library-ts/lib/big.js | 3 ++- tests/Js/Main/ArithmeticTests.fs | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 374d37c99a..ab79c6c700 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -969,7 +969,8 @@ P.toFixed = function (dp, rm) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { throw Error(INVALID_DP); } - x = round(new x.constructor(x), dp + x.e + 1, rm); + x = new x.constructor(x); + x = round(x, dp + x.e + 1, rm); // x.e may have changed if the value is rounded up. for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index ae655de116..a5fa090a8c 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -210,6 +210,14 @@ let tests = string (decimal "-0.5") |> equal "-0.5" string (decimal "-0.000") |> equal "0.000" + testCase "Decimal fixed-point formatting respects the precision" <| fun () -> + let inv = System.Globalization.CultureInfo.InvariantCulture + (decimal 0.001).ToString("F2", inv) |> equal "0.00" + (decimal 0.05).ToString("F1", inv) |> equal "0.1" + (decimal 0.001).ToString("N2", inv) |> equal "0.00" + (decimal "0.001").ToString("F2", inv) |> equal "0.00" + sprintf "%.2f" (decimal 0.001) |> equal "0.00" + testCase "Decimal division works" <| fun () -> let a = decimal 0.00001 let b = 1000.M From ef63087d97b61927763023c4b2e5bb16bcd64af2 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 10 Sep 2026 23:17:25 +0200 Subject: [PATCH 058/322] fix(js/ts): omit the decimal sign when the rounded result is zero --- src/fable-library-ts/lib/big.js | 9 ++++----- tests/Js/Main/ArithmeticTests.fs | 9 +++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index ab79c6c700..e2bb014635 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -958,12 +958,11 @@ P.toExponential = function (dp, rm) { * dp? {number} Decimal places: integer, 0 to MAX_DP inclusive. * rm? {number} Rounding mode: 0 (down), 1 (half-up), 2 (half-even) or 3 (up). * - * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'. - * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'. + * Unlike JavaScript, the sign is omitted when the rounded result is zero: + * (-0.1).toFixed(0) is '0' and (-0.01).toFixed(1) is '0.0', matching .NET. */ P.toFixed = function (dp, rm) { - var x = this, - n = hasNonzeroDigit(x); + var x = this; if (dp !== UNDEFINED) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { @@ -976,7 +975,7 @@ P.toFixed = function (dp, rm) { for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); } - return stringify(x, false, n); + return stringify(x, false, hasNonzeroDigit(x)); }; diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index a5fa090a8c..c369b71c52 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -218,6 +218,15 @@ let tests = (decimal "0.001").ToString("F2", inv) |> equal "0.00" sprintf "%.2f" (decimal 0.001) |> equal "0.00" + testCase "Decimal fixed-point formatting omits the sign when the result rounds to zero" <| fun () -> + let inv = System.Globalization.CultureInfo.InvariantCulture + (-0.001M).ToString("F2", inv) |> equal "0.00" + (-0.1M).ToString("F0", inv) |> equal "0" + (-0.4M).ToString("F0", inv) |> equal "0" + (-0.001M).ToString("N2", inv) |> equal "0.00" + (-0.6M).ToString("F0", inv) |> equal "-1" + (-1.5M).ToString("F0", inv) |> equal "-2" + testCase "Decimal division works" <| fun () -> let a = decimal 0.00001 let b = 1000.M From f34dda14e095f7bb3b26a782c1d8f95325650a11 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 18:10:18 +0200 Subject: [PATCH 059/322] refactor(python): make Naming.uniqueIndex private --- src/Fable.Transforms/Python/Prelude.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Python/Prelude.fs b/src/Fable.Transforms/Python/Prelude.fs index d14f71f8a6..133c90ed69 100644 --- a/src/Fable.Transforms/Python/Prelude.fs +++ b/src/Fable.Transforms/Python/Prelude.fs @@ -173,7 +173,7 @@ module Naming = let reflectionSuffix = "_reflection" - let mutable uniqueIndex = 0 + let mutable private uniqueIndex = 0 let getUniqueIndex () = let idx = uniqueIndex From f282ab48203fc5544c74b809f4396bb924724a2a Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 18:10:20 +0200 Subject: [PATCH 060/322] perf(standalone): accumulate compile errors in a ResizeArray --- src/fable-standalone/src/Worker/Worker.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 56451a9d8c..1097937b16 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -285,7 +285,7 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l ) () - let mutable errors = parseResults.Errors + let errors = ResizeArray parseResults.Errors let mutable fableTransformTime = 0. let compiledCode = ResizeArray() @@ -293,8 +293,8 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l for fileName in filesToEmit do let! code, fableErrors, transformTime = emitFile fable parseResults fileName language fableOptions - compiledCode.Add(code) - errors <- Array.append errors fableErrors + compiledCode.Add code + errors.AddRange fableErrors fableTransformTime <- fableTransformTime + transformTime let stats: CompileStats = @@ -304,7 +304,7 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l Fable_transform = fableTransformTime } - return (compiledCode.ToArray(), errors, stats) + return (compiledCode.ToArray(), errors.ToArray(), stats) } let private describeError (er: exn) = From 6055c0de354aabcb4e3fdd7d6a0723c1b991d70b Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 18:19:08 +0200 Subject: [PATCH 061/322] ci(dart): drop id-token from the Dart job --- .github/workflows/build.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fcd73438c8..c6b4933675 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -276,6 +276,10 @@ jobs: build-dart: runs-on: ubuntu-latest timeout-minutes: 45 + # Without id-token, setup-dart skips the pub.dev token, so packages are + # fetched anonymously. + permissions: + contents: read steps: - uses: actions/checkout@v7.0.1 @@ -286,11 +290,6 @@ jobs: - name: Setup Dart SDK uses: dart-lang/setup-dart@v1 - # setup-dart registers a pub.dev token, which pub then sends on dependency - # fetches too. This job only consumes packages, so drop it and stay anonymous. - - name: Use pub.dev anonymously - run: dart pub token remove https://pub.dev - - name: Fable Tests - Dart run: ./build.sh test dart From 6f1f1b8afd9158743978720507ced5c4a8b2144f Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:13:21 +0200 Subject: [PATCH 062/322] fix(ts): declare the type parameter of a flexible-type overload w --- src/Fable.Transforms/FSharp2Fable.Util.fs | 28 +++++++++++++---------- tests/Js/Main/TypeTests.fs | 16 +++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index acbd96349e..d9d9dd3317 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -12,7 +12,7 @@ open Fable.Transforms open Fable.Transforms.FSharp2Fable module Extensions = - let areParamTypesEqual genArgs (args1: Fable.Type array) (args2: IList>) = + let areParamTypesEqual strict genArgs (args1: Fable.Type array) (args2: IList>) = // Not entirely sure why, but it seems members with a single unit argument sometimes have this parameter // and sometimes none, so just to be sure always remove single unit arguments let args2 = @@ -24,7 +24,7 @@ module Extensions = if args1.Length = args2.Length then let args2 = args2 |> Array.map (fun p -> TypeHelpers.makeType genArgs p.Type) - Array.forall2 (typeEquals false) args1 args2 + Array.forall2 (typeEquals strict) args1 args2 else false @@ -432,19 +432,23 @@ type FsEnt(maybeAbbrevEnt: FSharpEntity) = ) // entity.EnumerateMembersFunctionsAndValues(?includeHierarchy=searchHierarchy) - members.Force() - |> Seq.tryFind (fun m -> - if + let candidates = + members.Force() + |> Seq.filter (fun m -> m.CompiledName = compiledName && m.IsInstanceMember = isInstance && (doNotRequireDispatchSlot || m.IsDispatchSlot) - then - match argTypes with - | Some argTypes -> Extensions.areParamTypesEqual genArgs argTypes m.CurriedParameterGroups - | None -> true - else - false - ) + ) + + match argTypes with + | None -> Seq.tryHead candidates + | Some argTypes -> + let hasParamTypes strict = + candidates + |> Seq.tryFind (fun m -> Extensions.areParamTypesEqual strict genArgs argTypes m.CurriedParameterGroups) + + // A loose comparison accepts a generic parameter as equal to any type + hasParamTypes true |> Option.orElseWith (fun () -> hasParamTypes false) interface Fable.Entity with member _.Ref = FsEnt.Ref ent diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 6de95523ff..a1a73d7a25 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -466,6 +466,15 @@ type Distinct1 = static member OfList(_elements: list<'K * 'V>) = () static member OfList(_elements: list) = () +type FlexibleOverloads() = + member _.For(xs: 'a list, f: 'a -> 'b list) : 'b list = xs |> List.collect f + member _.For(xs: #seq<'a>, f: 'a -> unit) : int = + let mutable count = 0 + for x in xs do + f x + count <- count + 1 + count + type InfoA = { Foo: string } @@ -1121,6 +1130,13 @@ let tests = t.Overload(2) |> equal 4 t.Overload(2, 3) |> equal 5 + testCase "Overloads distinguished by a flexible type work" <| fun () -> + let t = FlexibleOverloads() + t.For([1; 2], fun i -> [i; i * 10]) |> equal [1; 10; 2; 20] + let mutable sum = 0 + t.For([|1; 2; 3|], fun i -> sum <- sum + i) |> equal 3 + sum |> equal 6 + testCase "Type abbreviation works" <| fun () -> let t = T4() t.Value2 |> equal "Bye" From 584d8ce4f6d434df56195ed6d09d6336df1970d6 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:39:04 +0200 Subject: [PATCH 063/322] fix(python): parse uint64 values above Int64.MaxValue w --- src/fable-library-py/src/ints.rs | 6 ++++++ tests/Js/Main/ConvertTests.fs | 6 ++++++ tests/Python/TestConvert.fs | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/src/fable-library-py/src/ints.rs b/src/fable-library-py/src/ints.rs index 6580ed1b2f..258c558b8e 100644 --- a/src/fable-library-py/src/ints.rs +++ b/src/fable-library-py/src/ints.rs @@ -1311,6 +1311,12 @@ pub fn parse_int64( // value, not a two's complement bit pattern, so u64 cannot parse it i64::from_str_radix(&final_string, actual_radix as u32) .map_err(|_| create_parse_error(string))? + } else if unsigned { + // The upper half of the u64 range does not fit in an i64, so it is carried + // as its two's complement bit pattern and cast back by the caller + u64::from_str_radix(&final_string, actual_radix as u32) + .map(|u_val| u_val as i64) + .map_err(|_| create_parse_error(string))? } else { // For decimal, use standard i64 parsing i64::from_str_radix(&final_string, actual_radix as u32) diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index c1a23027bc..171ada44ad 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -192,6 +192,12 @@ let tests = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) + testCase "System.UInt64.TryParse works above Int64.MaxValue" <| fun () -> + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + testCase "Parsing integers with different radices works" <| fun () -> equal 11 (int "11") equal 17 (int "0x11") diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 95dba9c4cb..781b711289 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -197,6 +197,13 @@ let ``test System.UInt64.TryParse works`` () = tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) +[] +let ``test System.UInt64.TryParse works above Int64.MaxValue`` () = + tryParse UInt64.TryParse 0UL "9223372036854775808" |> equal (true, 9223372036854775808UL) + tryParse UInt64.TryParse 0UL "18446744073709551615" |> equal (true, UInt64.MaxValue) + tryParse UInt64.TryParse 0UL "18446744073709551616" |> equal (false, 0UL) + tryParse UInt64.TryParse 0UL "-1" |> equal (false, 0UL) + [] let ``test Parsing integers with different radices works`` () = equal 11 (int "11") From 4abf06ea86186f64a15078fb19c6006cca0676b0 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:39:09 +0200 Subject: [PATCH 064/322] fix(python): report DateTimeKind.Utc for DateTime.UtcNow --- src/fable-library-py/fable_library/date.py | 17 ++++++++++++----- tests/Python/TestDateTime.fs | 3 ++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/fable-library-py/fable_library/date.py b/src/fable-library-py/fable_library/date.py index 08dd229365..d8209c0500 100644 --- a/src/fable-library-py/fable_library/date.py +++ b/src/fable-library-py/fable_library/date.py @@ -50,6 +50,11 @@ ] +def _without_kind(d: datetime) -> datetime: + # .NET compares and subtracts the tick count and ignores DateTimeKind + return d.replace(tzinfo=None) + + @overload def subtract(x: datetime, y: datetime) -> TimeSpan: ... @overload @@ -58,7 +63,7 @@ def subtract(x: datetime, y: TimeSpan) -> datetime: ... def subtract(x: datetime, y: datetime | TimeSpan) -> datetime | TimeSpan: if isinstance(y, datetime): - delta = x - y + delta = _without_kind(x) - _without_kind(y) # ts.microseconds only contains the microseconds provided to the constructor # so we need to calculate the total microseconds ourselves delta_microseconds = delta.days * (24 * 3600 * 10**6) + delta.seconds * 10**6 + delta.microseconds @@ -604,7 +609,7 @@ def now() -> datetime: def utc_now() -> datetime: - return datetime.now(UTC).replace(tzinfo=None) + return datetime.now(UTC) def today() -> datetime: @@ -616,17 +621,19 @@ def to_local_time(date: datetime) -> datetime: def compare(x: datetime, y: datetime) -> int: - if x == y: + _x, _y = _without_kind(x), _without_kind(y) + + if _x == _y: return 0 - if x < y: + if _x < _y: return -1 return 1 def equals(x: datetime, y: datetime) -> bool: - return x == y + return _without_kind(x) == _without_kind(y) def max_value() -> datetime: diff --git a/tests/Python/TestDateTime.fs b/tests/Python/TestDateTime.fs index a1d06ff962..76e9af4218 100644 --- a/tests/Python/TestDateTime.fs +++ b/tests/Python/TestDateTime.fs @@ -925,7 +925,8 @@ let ``test DateTime.Now works`` () = let ``test DateTime.UtcNow works`` () = let d = DateTime.UtcNow d > DateTime.MinValue |> equal true - // d.Kind |> equal DateTimeKind.Utc // currently we're not storing Kind + d.Kind |> equal DateTimeKind.Utc + d.Date.Kind |> equal DateTimeKind.Utc [] let ``test DateTime.AddYears works`` () = From 0f564c744e86096c779d0acea6d677bce6d56ee1 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:39:15 +0200 Subject: [PATCH 065/322] fix(python): reflect an erased union as the type it erases to --- .../Python/Fable2Python.Reflection.fs | 43 +++++++++++++------ tests/Python/TestReflection.fs | 34 +++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 4d8d0a6254..c28508fd91 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -278,21 +278,38 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map List.map (transformTypeInfo com ctx r genMap) |> Helpers.unzipArgs - // Check if the entity is actually declared in Python code - if - ent.IsInterface - || FSharp2Fable.Util.isErasedOrStringEnumEntity ent - || FSharp2Fable.Util.isGlobalOrImportedEntity ent - || FSharp2Fable.Util.isReplacementCandidate entRef - then - genericEntity ent.FullName generics, stmts - else - let reflectionMethodExpr = - FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix - let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) + let erasedFieldType = + if ent.IsFSharpUnion && FSharp2Fable.Util.hasAttribute Atts.erase ent then + match ent.UnionCases with + | [ uci ] when List.isSingle uci.UnionCaseFields -> Some uci.UnionCaseFields.Head.FieldType + | _ -> None + else + None + + match erasedFieldType with + | Some fieldType -> + let fieldGenMap = + Seq.zip (ent.GenericParameters |> Seq.map (fun p -> p.Name)) generics |> Map + + let expr, stmts' = transformTypeInfo com ctx r fieldGenMap fieldType + expr, stmts @ stmts' + | None -> + // Check if the entity is actually declared in Python code + if + ent.IsInterface + || FSharp2Fable.Util.isErasedOrStringEnumEntity ent + || FSharp2Fable.Util.isGlobalOrImportedEntity ent + || FSharp2Fable.Util.isReplacementCandidate entRef + then + genericEntity ent.FullName generics, stmts + else + let reflectionMethodExpr = + FSharp2Fable.Util.entityIdentWithSuffix com entRef Naming.reflectionSuffix + + let callee, stmts' = com.TransformAsExpr(ctx, reflectionMethodExpr) - Expression.call (callee, generics), stmts @ stmts' + Expression.call (callee, generics), stmts @ stmts' let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = if ent.IsFSharpRecord then diff --git a/tests/Python/TestReflection.fs b/tests/Python/TestReflection.fs index 3ce9051c29..43c34cda20 100644 --- a/tests/Python/TestReflection.fs +++ b/tests/Python/TestReflection.fs @@ -664,5 +664,39 @@ let ``test Can get union values without type`` () = getCaseFields x |> equal [||] getCaseFields y |> equal [|5; "foo"|] +type ReactiveNode<'T>(value: 'T) = + member _.Value = value + +[] +type ErasedView<'T> = + | ErasedView of node: ReactiveNode<'T> + +type ModelWithErasedField = { Item: ErasedView } + +type DuWithErasedField = + | Wrapped of ErasedView + | Empty + +[] +let ``test An erased union reflects as the type it erases to`` () = + let erasedTyp = typeof> + let nodeTyp = typeof> + erasedTyp.FullName |> equal nodeTyp.FullName + FSharpType.IsUnion erasedTyp |> equal false + +[] +let ``test Reflection works for a record with an erased generic union field`` () = + let typ = typeof + FSharpType.IsRecord typ |> equal true + let fields = FSharpType.GetRecordFields typ + fields.Length |> equal 1 + fields.[0].PropertyType.GetGenericArguments().[0] |> equal typeof + +[] +let ``test Reflection works for a union with an erased generic union field`` () = + let typ = typeof + let case = (FSharpType.GetUnionCases typ).[0] + case.GetFields().[0].PropertyType.GetGenericArguments().[0] |> equal typeof + #endif #endif From cf0c9305759af17fde6dcd433618bbeaca5b60d0 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:39:29 +0200 Subject: [PATCH 066/322] fix(python): implement Decimal.GetBits and the bits constructors --- .../fable_library/decimal_.py | 47 ++++++++++++++++++- tests/Python/TestArithmetic.fs | 47 ++++++++++++------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/fable-library-py/fable_library/decimal_.py b/src/fable-library-py/fable_library/decimal_.py index d9e92a2861..e2756e4927 100644 --- a/src/fable-library-py/fable_library/decimal_.py +++ b/src/fable-library-py/fable_library/decimal_.py @@ -1,6 +1,6 @@ from decimal import MAX_EMAX, MIN_EMIN, Decimal, getcontext -from .core import FSharpRef, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 +from .core import Array, FSharpRef, array, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 from .types import IntegerTypes @@ -141,6 +141,48 @@ def from_parts( return value +def from_ints(low: IntegerTypes, mid: IntegerTypes, high: IntegerTypes, sign_exp: IntegerTypes) -> Decimal: + _sign_exp = int(sign_exp) + is_negative = 1 if _sign_exp < 0 else 0 + scale = (_sign_exp >> 16) & 0x7F + return from_parts(low, mid, high, is_negative, scale) + + +def from_int_array(bits: Array[int]) -> Decimal: + return from_ints(bits[0], bits[1], bits[2], bits[3]) + + +def _to_int32(value: int) -> int: + masked = value & 0xFFFFFFFF + return masked - 0x100000000 if masked >= 0x80000000 else masked + + +def get_bits(value: Decimal) -> Array[int]: + sign, digits, exponent = value.as_tuple() + + if not isinstance(exponent, int): + raise ValueError(f"The value {value} cannot be represented as a System.Decimal.") + + mantissa = int("".join(str(digit) for digit in digits)) + scale = 0 + + if exponent > 0: + mantissa *= 10**exponent + else: + scale = -exponent + + flags = ((scale & 0x7F) << 16) | (0x80000000 if sign else 0) + + return array.Int32Array( + [ + _to_int32(mantissa), + _to_int32(mantissa >> 32), + _to_int32(mantissa >> 64), + _to_int32(flags), + ] + ) + + def to_string(x: Decimal) -> str: return str(x) @@ -184,7 +226,10 @@ def create(value: float | float32 | IntegerTypes | str) -> Decimal: "compare", "divide", "equals", + "from_int_array", + "from_ints", "from_parts", + "get_bits", "max", "min", "multiply", diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 1bd98a9604..5b670346df 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -419,23 +419,36 @@ let ``test Decimal.Negate works`` () = let ``test Decimal Evaluation order is preserved by generated code`` () = (4.4567M - 2.2234M) * 2.6492M + 1.2493M |> equal 7.16575836M -// [] -// let ``test Decimal constructors work`` () = -// let d = 1.2493M -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) -// d2 |> equal d -// d3 |> equal d - -// [] -// let ``test Decimal GetBits works`` () = -// let d = Decimal([| -1; -1; -2; 0 |]) -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) -// d2 |> equal d -// d3 |> equal -d +[] +let ``test Decimal constructors work`` () = + let d = 1.2493M + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], false, 4uy) + d2 |> equal d + d3 |> equal d + +[] +let ``test Decimal GetBits works`` () = + let d = Decimal([| -1; -1; -2; 0 |]) + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits[0], bits[1], bits[2], true, 0uy) + d2 |> equal d + d3 |> equal -d + +[] +let ``test Decimal GetBits works with arithmetic operations`` () = + let check (d: decimal) = + let bits = Decimal.GetBits(d) + Decimal(bits) |> equal d + + check (10M + 10M) + check (100.5M - 0.5M) + check (5M * 200M) + check (-0.1M * 100M) + check (2000M / 10M) + check (1.5M + 0.5M) [] let ``test Decimal abs works`` () = From 4a441b1926387a42db6a3a4294acd97b02760ca3 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 11 Sep 2026 19:43:51 +0200 Subject: [PATCH 067/322] fix(python): round a float to .NET's precision when converting to decimal --- src/fable-library-py/fable_library/decimal_.py | 18 +++++++++++++++--- tests/Python/TestArithmetic.fs | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/fable-library-py/fable_library/decimal_.py b/src/fable-library-py/fable_library/decimal_.py index e2756e4927..361c146f99 100644 --- a/src/fable-library-py/fable_library/decimal_.py +++ b/src/fable-library-py/fable_library/decimal_.py @@ -207,14 +207,26 @@ def try_parse(string: str, def_value: FSharpRef[Decimal]) -> bool: return False +def _from_float(value: float, significant_digits: int) -> Decimal: + rounded = Decimal(f"{value:.{significant_digits}G}") + + if rounded.is_zero(): + return get_zero + + # `Decimal` keeps the exponent it was parsed with, which would print as 1E+20 + return Decimal(format(rounded, "f")) + + def create(value: float | float32 | IntegerTypes | str) -> Decimal: match value: - # Int32 and Float64 are plain `int`/`float`, which `Decimal` already accepts - # via the last case + # Int32 is a plain `int`, which `Decimal` already accepts via the last case case sbyte() | byte() | int16() | uint16() | uint32() | int64() | uint64(): return Decimal(int(value)) + # .NET rounds to 7 significant digits for Single and 15 for Double case float32(): - return Decimal(float(value)) + return _from_float(float(value), 7) + case float(): + return _from_float(value, 15) case _: return Decimal(value) diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 5b670346df..60f5d5612c 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -450,6 +450,21 @@ let ``test Decimal GetBits works with arithmetic operations`` () = check (2000M / 10M) check (1.5M + 0.5M) +[] +let ``test Decimal from a float rounds like .NET`` () = + string (decimal 0.1) |> equal "0.1" + string (decimal -0.1) |> equal "-0.1" + string (decimal 0.3) |> equal "0.3" + string (decimal 1.1) |> equal "1.1" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + +[] +let ``test Decimal from a float32 rounds like .NET`` () = + string (decimal 0.1f) |> equal "0.1" + string (decimal -0.1f) |> equal "-0.1" + string (decimal (1.0f / 3.0f)) |> equal "0.3333333" + [] let ``test Decimal abs works`` () = abs -4M |> equal 4M From 9245d5f89574ce0ca44f9acdfae0831123ae7f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A4=96=20easybuild-shipit?= Date: Fri, 11 Sep 2026 18:45:37 +0000 Subject: [PATCH 068/322] chore: release multiple projects --- src/Fable.Cli/CHANGELOG.md | 20 +++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 20 +++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 16 +++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 12 +++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 16 +++++++++++++++- src/fable-standalone/package.json | 4 ++-- 9 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 0ed8d23e31..b631aab8f1 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 +last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.17.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Support bytes() on generic and bool arrays (#4961) ([94b59294](https://github.com/fable-compiler/Fable/commit/94b59294b4749c1f871484b843604da81ceb71ad)) +* *(python)* Parse uint64 values above Int64.MaxValue ([584d8ce4](https://github.com/fable-compiler/Fable/commit/584d8ce4f6d434df56195ed6d09d6336df1970d6)) +* *(python)* Report DateTimeKind.Utc for DateTime.UtcNow ([4abf06ea](https://github.com/fable-compiler/Fable/commit/4abf06ea86186f64a15078fb19c6006cca0676b0)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(python)* Implement Decimal.GetBits and the bits constructors ([cf0c9305](https://github.com/fable-compiler/Fable/commit/cf0c9305759af17fde6dcd433618bbeaca5b60d0)) +* *(python)* Round a float to .NET's precision when converting to decimal ([4a441b19](https://github.com/fable-compiler/Fable/commit/4a441b1926387a42db6a3a4294acd97b02760ca3)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + ## 5.17.0 - 2026-09-08 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 259c4937c0..0518010ea6 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 +last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.16.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Support bytes() on generic and bool arrays (#4961) ([94b59294](https://github.com/fable-compiler/Fable/commit/94b59294b4749c1f871484b843604da81ceb71ad)) +* *(python)* Parse uint64 values above Int64.MaxValue ([584d8ce4](https://github.com/fable-compiler/Fable/commit/584d8ce4f6d434df56195ed6d09d6336df1970d6)) +* *(python)* Report DateTimeKind.Utc for DateTime.UtcNow ([4abf06ea](https://github.com/fable-compiler/Fable/commit/4abf06ea86186f64a15078fb19c6006cca0676b0)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(python)* Implement Decimal.GetBits and the bits constructors ([cf0c9305](https://github.com/fable-compiler/Fable/commit/cf0c9305759af17fde6dcd433618bbeaca5b60d0)) +* *(python)* Round a float to .NET's precision when converting to decimal ([4a441b19](https://github.com/fable-compiler/Fable/commit/4a441b1926387a42db6a3a4294acd97b02760ca3)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + ## 5.16.0 - 2026-09-08 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 4b01aa917d..e5052f0b1f 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.17.0" + let VERSION = "5.17.1" [] - let JS_LIBRARY_VERSION = "2.7.0" + let JS_LIBRARY_VERSION = "2.7.1" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 3cdeadcc4f..c84662181c 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 +last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.4.0 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +### ⚡ Performance Improvements + +* *(standalone)* Accumulate compile errors in a ResizeArray ([f282ab48](https://github.com/fable-compiler/Fable/commit/f282ab48203fc5544c74b809f4396bb924724a2a)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + ## 3.3.0 - 2026-09-08 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 68f638dbe4..5d818b15dd 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.3.0", + "version": "3.4.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.3.0" + "@fable-org/fable-standalone": "^3.4.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 1777fcc54e..e083a94ebf 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 +last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 updaters: - package.json: file: package.json @@ -15,6 +15,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.7.1 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(js/ts)* Keep the sign of negative decimals below 0.1 ([91d80f18](https://github.com/fable-compiler/Fable/commit/91d80f185fb38e4d23fd9e906037a14daab3e3b9)) +* *(js/ts)* Respect the precision when formatting decimals below 0.1 ([1c02557e](https://github.com/fable-compiler/Fable/commit/1c02557efd113d8d30a922c74c3fc9d1e7168cfd)) +* *(js/ts)* Omit the decimal sign when the rounded result is zero ([ef63087d](https://github.com/fable-compiler/Fable/commit/ef63087d97b61927763023c4b2e5bb16bcd64af2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + ## 2.7.0 - 2026-09-08 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index d7387fd226..4145cefb10 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.7.0", + "version": "2.7.1", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index f6bf96616c..304614ed0e 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c49d1ec5d1a491bfff65fe4bd6c71014cac9b321 +last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -19,6 +19,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.4.0 - 2026-09-11 + +### 🐞 Bug Fixes + +* *(python)* Preserve record field names in structured formatting (#4952) ([fbc162d6](https://github.com/fable-compiler/Fable/commit/fbc162d6912d62b9a75e73f8264814f6519b6194)) +* *(python)* Reflect an erased union as the type it erases to ([0f564c74](https://github.com/fable-compiler/Fable/commit/0f564c744e86096c779d0acea6d677bce6d56ee1)) +* *(ts)* Declare the type parameter of a flexible-type overload ([6f1f1b8a](https://github.com/fable-compiler/Fable/commit/6f1f1b8afd9158743978720507ced5c4a8b2144f)) + +### ⚡ Performance Improvements + +* *(standalone)* Accumulate compile errors in a ResizeArray ([f282ab48](https://github.com/fable-compiler/Fable/commit/f282ab48203fc5544c74b809f4396bb924724a2a)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c49d1ec5d1a491bfff65fe4bd6c71014cac9b321..4a441b1926387a42db6a3a4294acd97b02760ca3) + ## 3.3.0 - 2026-09-08 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 1a20c1e394..3c4d00e0aa 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.3.0", - "fableVersion": "5.17.0", + "version": "3.4.0", + "fableVersion": "5.17.1", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 66efd10960c967660084db2b364e944d167897f4 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sat, 12 Sep 2026 21:38:44 +0200 Subject: [PATCH 069/322] fix(python): use the .NET range for Decimal.MaxValue and Decimal.MinValue (#4967) --- src/fable-library-py/fable_library/decimal_.py | 6 +++--- tests/Python/TestArithmetic.fs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fable-library-py/fable_library/decimal_.py b/src/fable-library-py/fable_library/decimal_.py index 361c146f99..6d20a9aa42 100644 --- a/src/fable-library-py/fable_library/decimal_.py +++ b/src/fable-library-py/fable_library/decimal_.py @@ -1,4 +1,4 @@ -from decimal import MAX_EMAX, MIN_EMIN, Decimal, getcontext +from decimal import Decimal, getcontext from .core import Array, FSharpRef, array, byte, float32, float64, int16, int64, sbyte, uint16, uint32, uint64 from .types import IntegerTypes @@ -10,8 +10,8 @@ get_one = Decimal(1) get_minus_one = Decimal(-1) -get_max_value = MAX_EMAX -get_min_value = MIN_EMIN +get_max_value = Decimal("79228162514264337593543950335") +get_min_value = Decimal("-79228162514264337593543950335") def compare(x: Decimal, y: Decimal) -> int: diff --git a/tests/Python/TestArithmetic.fs b/tests/Python/TestArithmetic.fs index 60f5d5612c..2cba8c54c2 100644 --- a/tests/Python/TestArithmetic.fs +++ b/tests/Python/TestArithmetic.fs @@ -321,8 +321,8 @@ let ``test Decimal literals can be generated`` () = 0M |> equal Decimal.Zero 1M |> equal Decimal.One -1M |> equal Decimal.MinusOne - // FIXME: 79228162514264337593543950335M |> equal Decimal.MaxValue - // FIXME: -79228162514264337593543950335M |> equal Decimal.MinValue + 79228162514264337593543950335M |> equal Decimal.MaxValue + -79228162514264337593543950335M |> equal Decimal.MinValue [] let ``test Decimal.ToString works`` () = From 42a9d6cdb9400504f966184ec9d989f202016a07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:06:11 +0200 Subject: [PATCH 070/322] chore: release multiple projects (#4968) --- src/Fable.Cli/CHANGELOG.md | 10 +++++++++- src/Fable.Compiler/CHANGELOG.md | 10 +++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-standalone/package.json | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index b631aab8f1..7bae9f33e9 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 +last_commit_released: 66efd10960c967660084db2b364e944d167897f4 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.17.2 - 2026-09-12 + +### 🐞 Bug Fixes + +* *(python)* Use the .NET range for Decimal.MaxValue and Decimal.MinValue (#4967) ([66efd109](https://github.com/fable-compiler/Fable/commit/66efd10960c967660084db2b364e944d167897f4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..66efd10960c967660084db2b364e944d167897f4) + ## 5.17.1 - 2026-09-11 ### 🐞 Bug Fixes diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 0518010ea6..52c450c12e 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 +last_commit_released: 66efd10960c967660084db2b364e944d167897f4 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.16.2 - 2026-09-12 + +### 🐞 Bug Fixes + +* *(python)* Use the .NET range for Decimal.MaxValue and Decimal.MinValue (#4967) ([66efd109](https://github.com/fable-compiler/Fable/commit/66efd10960c967660084db2b364e944d167897f4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..66efd10960c967660084db2b364e944d167897f4) + ## 5.16.1 - 2026-09-11 ### 🐞 Bug Fixes diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index e5052f0b1f..806a2d2a10 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.17.1" + let VERSION = "5.17.2" [] let JS_LIBRARY_VERSION = "2.7.1" diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 3c4d00e0aa..9ba4d8e3c0 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -3,7 +3,7 @@ "name": "@fable-org/fable-standalone", "private": false, "version": "3.4.0", - "fableVersion": "5.17.1", + "fableVersion": "5.17.2", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From ad9bf3eefcbedb753a68e4f4ff757176be2a0d56 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sat, 12 Sep 2026 22:13:46 +0200 Subject: [PATCH 071/322] chore(standalone): release when fable-library-ts changes --- src/fable-standalone/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 304614ed0e..5d5289deda 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -4,6 +4,7 @@ include: - ../Fable.Transforms/ - ../fcs-fable/ - ../Fable.AST/ + - ../fable-library-ts/ updaters: - package.json: file: package.json From 5d62380cb30dc1080c6943445a256e27db6246f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:39:51 -0700 Subject: [PATCH 072/322] fix(rust): Less allocations when enumerating strings (#4970) --- src/Fable.Transforms/Rust/Replacements.fs | 19 ++---- src/fable-library-rust/src/String.rs | 73 ++++++++++++++++++----- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index cc4dbae578..2181acbdc5 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -330,8 +330,8 @@ let toList com t (expr: Expr) = | List _ -> expr | Array _ -> Helper.LibCall(com, "List", "ofArray", t, [ expr ]) | String -> - let chars = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - Helper.LibCall(com, "List", "ofArray", t, [ chars ]) + let chars = Helper.LibCall(com, "String", "toSeq", t, [ expr ]) + Helper.LibCall(com, "List", "ofSeq", t, [ chars ]) | IEnumerable -> Helper.LibCall(com, "List", "ofSeq", t, [ expr ]) | _ -> TypeCast(expr, t) @@ -342,9 +342,7 @@ let toSeq com t (expr: Expr) = | Array _ -> Helper.LibCall(com, "Seq", "ofArray", t, [ expr ]) | Builtin(FSharpMap _) -> Helper.LibCall(com, "Map", "toEnumerable", t, [ expr ]) | Builtin(FSharpSet _) -> Helper.LibCall(com, "Set", "toEnumerable", t, [ expr ]) - | String -> - let chars = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - Helper.LibCall(com, "Seq", "ofArray", t, [ chars ]) + | String -> Helper.LibCall(com, "String", "toSeq", t, [ expr ]) | _ -> TypeCast(expr, t) let emitRawString (s: string) = $"\"%s{s}\"" |> emitExpr None String [] @@ -1391,11 +1389,9 @@ let getEnumerator com r t i (expr: Expr) = // | IsEntity (Types.regexCaptureCollection) _ | Array _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ expr ], ?loc = r) | List _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ expr ], ?loc = r) - // A string is enumerable in F# but `string` is `LrcStr` in Rust, with no - // GetEnumerator to fall through to -- so `for ch in s do` did not compile. | String -> - let ar = Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) + let en = toSeq com Any expr + makeInstanceCall r t i en "GetEnumerator" [] | IsEntity (Types.hashset) _ | IsEntity (Types.iset) _ -> let ar = Helper.LibCall(com, "HashSet", "entries", t, [ expr ]) @@ -1404,11 +1400,8 @@ let getEnumerator com r t i (expr: Expr) = | IsEntity (Types.idictionary) _ | IsEntity (Types.ireadonlydictionary) _ -> let ar = Helper.LibCall(com, "HashMap", "entries", t, [ expr ], [ expr.Type ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) - | _ -> - // Helper.LibCall(com, "Util", "getEnumerator", t, [toSeq com Any expr], ?loc=r) - makeInstanceCall r t i expr "GetEnumerator" [] + | _ -> makeInstanceCall r t i expr "GetEnumerator" [] let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = // let isIgnoreCase args = diff --git a/src/fable-library-rust/src/String.rs b/src/fable-library-rust/src/String.rs index 86dcab77c2..0cbf40b2a9 100644 --- a/src/fable-library-rust/src/String.rs +++ b/src/fable-library-rust/src/String.rs @@ -5,7 +5,7 @@ pub mod String_ { // Strings // ----------------------------------------------------------- - use crate::Native_::{compare, getHashCode, Func1, Func2, Hashable, NullableRef, String, ToString, Vec}; + use crate::Native_::{compare, iter_to_seq, getHashCode, seq_to_iter, Func1, Func2, Hashable, NullableRef, Seq, String, ToString, Vec}; use crate::NativeArray_::{array_from, Array}; use core::cmp::Ordering; @@ -54,8 +54,11 @@ pub mod String_ { LrcStr(Some(Lrc::from(s))) } - pub fn fromIter(iter: impl Iterator + Clone) -> string { - let s = iter.collect::(); + pub fn fromIter(it: I) -> string + where + I: Iterator + { + let s = it.collect::(); LrcStr(Some(Lrc::from(s))) } } @@ -130,26 +133,33 @@ pub mod String_ { fromSlice(s.as_str()) } - pub fn fromIter(iter: impl Iterator + Clone) -> string { - let len: usize = iter - .clone() - .take(INLINE_MAX + 1) - .map(|c| c.len_utf8()) - .sum(); - if len <= INLINE_MAX { - let mut buf = [0u8; INLINE_MAX]; - let mut pos: usize = 0; - for c in iter { + pub fn fromIter(it: I) -> string + where + I: Iterator + { + let mut len: usize = 0; + let mut pos: usize = 0; + let mut buf = [0u8; INLINE_MAX]; + let mut str: String = String::new(); + for c in it { + len += c.len_utf8(); + if len <= INLINE_MAX { let s = c.encode_utf8(&mut buf[pos..]); pos = pos + s.len(); + } else { + if str.len() == 0 { + str.push_str(core::str::from_utf8(&buf[0..pos]).unwrap()); + } + str.push(c); } + } + if len <= INLINE_MAX { LrcStr::Inline { len: len as u8, buf: buf, } } else { - let s = iter.collect::(); - LrcStr::Shared(Lrc::from(s)) + LrcStr::Shared(Lrc::from(str)) } } } @@ -318,6 +328,39 @@ pub mod String_ { } } + // ----------------------------------------------------------- + // IEnumerable + // ----------------------------------------------------------- + + struct StringIterator { + str: string, + pos: usize, + } + + impl Iterator for StringIterator { + type Item = char; + + fn next(&mut self) -> Option { + match self.str.as_str()[self.pos..].chars().next() { + Some(c) => { + self.pos += c.len_utf8(); + Some(c) + }, + None => { None } + } + } + } + + pub fn toSeq(s: string) -> Seq { + let it = StringIterator { str: s, pos: 0 }; + iter_to_seq(it) + } + + pub fn ofSeq(seq: Seq) -> string { + let it = seq_to_iter(seq); + fromIter(it) + } + // ----------------------------------------------------------- // string implementation // ----------------------------------------------------------- From 74adfa6e90e396941cc61d119934dd97579498a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:38:27 +0200 Subject: [PATCH 073/322] chore: Bump the all-pip group with 3 updates (#4975) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 6 ++-- uv.lock | 88 +++++++++++++++++++++++++------------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf3a155ec3..dfd280e297 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.6,<0.17", + "ruff>=0.16.7,<0.17", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.5", "attrs>=26.1.0", - "pyright>=1.1.411", - "ty>=0.0.78", + "pyright>=1.1.414", + "ty>=0.0.80", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 6385c35543..9fd0e23a62 100644 --- a/uv.lock +++ b/uv.lock @@ -66,10 +66,10 @@ dev = [ { name = "dunamai", specifier = ">=1.26.2,<2" }, { name = "maturin", specifier = ">=1.15.0,<2" }, { name = "pydantic", specifier = ">=2.13.5" }, - { name = "pyright", specifier = ">=1.1.411" }, + { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.6,<0.17" }, - { name = "ty", specifier = ">=0.0.78" }, + { name = "ruff", specifier = ">=0.16.7,<0.17" }, + { name = "ty", specifier = ">=0.0.80" }, ] [[package]] @@ -230,15 +230,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.6" +version = "0.16.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, - { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, - { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, - { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, - { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, - { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, - { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, - { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, + { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, ] [[package]] name = "ty" -version = "0.0.78" +version = "0.0.80" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/c7/2ba0861384c5b5097ac354383abb98188112cb330208c21d5197e98a29e5/ty-0.0.78.tar.gz", hash = "sha256:770b45854f85fa11595208f08c0f28df80943164d10a2832d86be6ac29f135b2", size = 7050609, upload-time = "2026-09-02T22:41:33.92Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/b0/6d1b10e0d422736a3c439e487c950ae785f401d71ff879d5be68bccb6d90/ty-0.0.80.tar.gz", hash = "sha256:fe86bc91327e45ff5e3593b7e306e7f57a44bc0608f38d647b8d97bd99c96013", size = 7183998, upload-time = "2026-09-09T21:18:47.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ed/f34cfc06a9ba72979df219e48dae1a0b6a6c74a340763ccd30a547edf913/ty-0.0.78-py3-none-linux_armv6l.whl", hash = "sha256:122700b98f9d45785c1ce91a9418154562e7f64f4bf34679f6f7b38c5b97453f", size = 13304624, upload-time = "2026-09-02T22:40:56.649Z" }, - { url = "https://files.pythonhosted.org/packages/1c/28/5576e2a08b57676d9b2a736d528f077a6c6e32c3d1de2d75dbbe28346966/ty-0.0.78-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbe7e3709ccf29ef3d9f58f5914cc30ec36fb647008a5a4ff8483abe401bfe8d", size = 12928383, upload-time = "2026-09-02T22:40:59.162Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/027d49c3da5235e634da3d3fc52c4874ec89f50830388c9c259f8fb71e0e/ty-0.0.78-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c3528897b3ab9d3589561bc2b5e61a4d5d686de527a4400bb09a439b922a6c70", size = 12730058, upload-time = "2026-09-02T22:41:01.235Z" }, - { url = "https://files.pythonhosted.org/packages/bb/bc/6bf8ffefd8063730a70ae889cf85def72bc155fd1453135ebf8a09c2f1c2/ty-0.0.78-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bfba4c44a06484093f527b8ef43a317abcf91395b49396170266629eedf74aa", size = 12813321, upload-time = "2026-09-02T22:41:03.251Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c0/b3cdf26f82108908d92fdb7ddb741019c446ceb666cf204d4dbf611bde33/ty-0.0.78-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f903c06fdee17baf8373173039ab28f1bce28e66b1c734929a5f50b37eb54d9", size = 13072219, upload-time = "2026-09-02T22:41:05.225Z" }, - { url = "https://files.pythonhosted.org/packages/4d/36/16bfb0abdd178dae11dbc4b57b9a86c2b62642a18c1103fd2c2930aa5879/ty-0.0.78-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd83e5fe3f07291d1bd4e591f8d2607c204f3eab53bcb1b8060c40e876e1aa61", size = 13903958, upload-time = "2026-09-02T22:41:07.333Z" }, - { url = "https://files.pythonhosted.org/packages/0f/0c/74d3b1f0344b13156c719dd68a7edf7e48c2051718c9f326ba3cbf45ea53/ty-0.0.78-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325285377319cf7168a2b8b771ae5027f411530e3c7eab1faf4583cdd72fd7c9", size = 14355581, upload-time = "2026-09-02T22:41:09.56Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d7/7ca0359e1e61b15b8b02328c8d120db3c507876b9b7c6bd9f26935353e0e/ty-0.0.78-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b7846a404da6492697b524a769755ee9bee157d67674063ecd9c5f69dc52ff", size = 14044749, upload-time = "2026-09-02T22:41:11.678Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6a/731f16ff42c5fc96e742c2f4e0a6915356bae114ae02ae4af6f33502175f/ty-0.0.78-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:169d3b9d134c0b48fe1af8a844142d374655552054a9d6c15a4b6e51bb0382ea", size = 13391647, upload-time = "2026-09-02T22:41:13.928Z" }, - { url = "https://files.pythonhosted.org/packages/0c/af/250cc29daf310e837188509ea4d78460c495d8a74c4fb84b4152d9ef2d4f/ty-0.0.78-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6108cb3b2d28dac5981d4e25008a0a5547d8c877a67f6da9e8c38e7e946be44f", size = 13945020, upload-time = "2026-09-02T22:41:15.968Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/96aab1dee4535e66554e8dc7657c69f6c61c181d8e70b9c3527908e93ef4/ty-0.0.78-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:589ad03608d9d2975ef4b23c41c4f847e325bcc7686f51d4c66066b8dbc18a2a", size = 12851149, upload-time = "2026-09-02T22:41:18.06Z" }, - { url = "https://files.pythonhosted.org/packages/85/b0/53d8fe9a847534ef5fe2165c7d5292cb853b29dfd7011cbfb1cdb90a8012/ty-0.0.78-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b3fa0786edc1af06030f872d83499c0cea7c261dbbb651e0aee8306bf2c8a869", size = 13091464, upload-time = "2026-09-02T22:41:20.227Z" }, - { url = "https://files.pythonhosted.org/packages/21/65/94a4e5a02de559f6c6c0ee7a14b88660b4eee058ec6fec5c0ff6ecfbf5cc/ty-0.0.78-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92c5639befc577578c8abd4e5a7fccf98d828db98b09e8d4dd81605dc0e54aef", size = 13389389, upload-time = "2026-09-02T22:41:22.27Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b6/d0c7fe6be64ca5a15100c4b1f0e39c19660d53bc544f609fa117b346e661/ty-0.0.78-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0dce70bc51652b2775debd1d1e422fb0179f5207c73deb4d5d0aca37e5f61695", size = 13691928, upload-time = "2026-09-02T22:41:24.292Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5d/36081205611ba3fa802efc4ad63e4b1e949bda2f7bb37b34ac9bb6c00db0/ty-0.0.78-py3-none-win32.whl", hash = "sha256:1c80976ca9185d7a9d1baab1fb57240331b8dac58d3b11e46200284086a6de8d", size = 12647346, upload-time = "2026-09-02T22:41:26.699Z" }, - { url = "https://files.pythonhosted.org/packages/f1/89/b925fe1ea1bc56fc7f11d2496e29072fdd2ceb7b389884fc7b2d07cf0c41/ty-0.0.78-py3-none-win_amd64.whl", hash = "sha256:32e82b704471eab34f67b51c151660ca8a00815977b28278905d76fba54f7415", size = 13241810, upload-time = "2026-09-02T22:41:28.857Z" }, - { url = "https://files.pythonhosted.org/packages/91/f1/090ef7b52355bcedfbfbff6ce70fa81ba5bd5f6ed3f8d99ae05e6f2fe75b/ty-0.0.78-py3-none-win_arm64.whl", hash = "sha256:3a14d641a3c04fa9a80f2a46be1531d915f60d4fb79d4b894627bbe46bb35d64", size = 13077433, upload-time = "2026-09-02T22:41:31.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/c8/3c93195eca282936ebb574d0ba98843e4b147ee324006f88464777012a92/ty-0.0.80-py3-none-linux_armv6l.whl", hash = "sha256:738d1cfca466c577aea24547c348629c00c63548cf7da2c46b497b3840f85dee", size = 13606509, upload-time = "2026-09-09T21:18:10.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/48/bbb47f7001c97262a5109bcd92a45bcc8a2fbb21e994c214a9897630bfb7/ty-0.0.80-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:56060164bb8ee43770fa367524fbdba2611fc90f8923a02cc91f80eb37d5a1b0", size = 13203812, upload-time = "2026-09-09T21:18:12.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/fd141160567047b742e466be8ed09a4c4ed80fa045d37f946eb4f4532fee/ty-0.0.80-py3-none-macosx_11_0_arm64.whl", hash = "sha256:da4062e0fbf3923d9b71688157c5753394f243348f00732e9edbb27498397169", size = 13021896, upload-time = "2026-09-09T21:18:15.186Z" }, + { url = "https://files.pythonhosted.org/packages/1e/10/b4177faf9e71bc37bc4d08f512042269660a1ce0ec457c48f96e51fc91d7/ty-0.0.80-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9eb94b2659f506a3a07a9ea1415d5c18aaa1e554adc1612614d617a0ed320e1", size = 13083881, upload-time = "2026-09-09T21:18:17.616Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c2/192d48b9a9acbbe030fc9a4bd73af75671a5566e731949f77e912a68ce68/ty-0.0.80-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f5e39a87da48c1af1a3b3135f02be7a83a93da30b0d6a5f5d27806e7c747ce", size = 13354874, upload-time = "2026-09-09T21:18:19.744Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/736dfd31efd98bed9dbe018ff91676e5ea83485b1e24afb635fd0247728f/ty-0.0.80-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e00149e7779c6b98f3ba12e10a631a861bd7ca1a42bbf064e2677ada7d2c0c2", size = 14204805, upload-time = "2026-09-09T21:18:21.999Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/557c726cb53401998e3589bb632c963a6887a70240e0a4386024f3731c81/ty-0.0.80-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a377268f359fb6e7a2cc9026a09223c764f58d020dec8e6baeaba9caba6ff829", size = 14630014, upload-time = "2026-09-09T21:18:24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/19/7a4b18fe27f6b6bd4ffa56b67c8b09ecb76bb4312d1c64b73f65417b4dbd/ty-0.0.80-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4511dbf1b266b9ce5b73e9b28d94468096c2b6da00e2fe205168c32d3b352ca7", size = 14326946, upload-time = "2026-09-09T21:18:26.679Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/16dd90805fc7e53ec54ae9fdb1a8b74753fb159a55a170f60f49b9417824/ty-0.0.80-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe95feffa7156800c6f804195acb9fb5846a39671c7192c30fff23351aaf7c31", size = 13705988, upload-time = "2026-09-09T21:18:28.681Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/4e87992c23ab8a742c7d4914ac5fe66fb9301c8464a69fd2ecbc0be3fbcd/ty-0.0.80-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba26b39f06bc8c3c2c5acd3a147239482b36620cdbca1b9d02e915294854f2cc", size = 14233729, upload-time = "2026-09-09T21:18:30.961Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/b8c11fda8e66a1d1cc1a7160ed719a33f4dc0107b1cef6a14e2673965763/ty-0.0.80-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e6167320888c115a6fbe69893b63fd1c848f9121a454e75e5f612674528e3f3", size = 13173523, upload-time = "2026-09-09T21:18:33.266Z" }, + { url = "https://files.pythonhosted.org/packages/73/a7/512083fa540c5be1ac8642658f03bfb5bfa6fd168ea4f55e3c0aabe04166/ty-0.0.80-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7aecb62b4de70b479eab07d1779ea66da26dd8b068f50a9330867157312f103c", size = 13373014, upload-time = "2026-09-09T21:18:35.339Z" }, + { url = "https://files.pythonhosted.org/packages/5e/63/cd2f0ca81fd9b8cafef93bbcf022bf636bc4de8ee7465c6aafeec1d4d52b/ty-0.0.80-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4fed06adacd16b7e2d722f37449021b1419598d0440769701c0f4827faddde63", size = 13667827, upload-time = "2026-09-09T21:18:37.241Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/ebdfdcb2dc099ae74dc4b75511eef0dd398b2fa27006c543543974b4567f/ty-0.0.80-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09329af6303ce611ec2cfffc995dc1ddf76c9f47194fc1a4d64a984759c25f5b", size = 13963866, upload-time = "2026-09-09T21:18:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/c286d5e1b560cfd16d6e91fdab683718aacf09fc88938ca06dcc9d1c8502/ty-0.0.80-py3-none-win32.whl", hash = "sha256:a81b3b512f7b4c68e42fbd1835633de658809666e9aafd5defa52bbc5197698d", size = 12881806, upload-time = "2026-09-09T21:18:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c8/3b3a8ac16d47a23ff34bf57c0d99a491860748aea96e0e886b4bfed54f9b/ty-0.0.80-py3-none-win_amd64.whl", hash = "sha256:8043f99878a2ae434781cb881c8a3840dff70c4a5b60bdc5ae84251f35ee063f", size = 13528883, upload-time = "2026-09-09T21:18:43.687Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/a6c697930fca76596d7d17f8853120f41a298fd9ea632c901cfca8ca869b/ty-0.0.80-py3-none-win_arm64.whl", hash = "sha256:e277ef034331da5319efc839c064968d24f35715b71c70369cf97d36fe4dcbdb", size = 13370135, upload-time = "2026-09-09T21:18:45.758Z" }, ] [[package]] From efee8a0bf703fc051fa3d36669082b67f1ea2e8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:39:32 +0200 Subject: [PATCH 074/322] chore: Bump the all-pip group in /src/fable-library-py with 2 updates (#4973) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 170 +++++++++++++++++------------------ 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index 19fc7e61d1..db8b5315f3 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,94 +61,94 @@ dev = [ [[package]] name = "hypothesis" -version = "6.167.1" +version = "6.168.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8cee74c1390b2932406faaab76980f18946f258fa5a8afca17189b3bc655/hypothesis-6.167.1.tar.gz", hash = "sha256:62eefcb4d2791423626e9901c3027a6e0c5ffda2ac0b44b3c7e797ab9d2d5a4c", size = 505849, upload-time = "2026-08-30T19:53:09.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/ce/c0946bebffb99b62426a6a7643d4272cc6c5cf777a488b3b4d0ee724e960/hypothesis-6.168.0.tar.gz", hash = "sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e", size = 510805, upload-time = "2026-09-08T18:48:36.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/4d/3592ca336deafbd3e9b0f47dc4c727aa32d30e765ef6370da8ecd590d388/hypothesis-6.167.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d28118fd70e4e15ff9c308a98b312b544b6145ae45aaa3b566328c1fdee8058f", size = 785476, upload-time = "2026-08-30T19:51:02.154Z" }, - { url = "https://files.pythonhosted.org/packages/18/bf/e33c431148994cbcb3332c6df94b833ecfb4aa6a8e51ea4b83da55ddd581/hypothesis-6.167.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e517be7f82a0a917758cc489a88b826b5371f56381fd94b4a8a09ce82d8de406", size = 781033, upload-time = "2026-08-30T19:51:27.314Z" }, - { url = "https://files.pythonhosted.org/packages/94/a3/e0de9a82c7e790a1def0801076e0ef43110f98e95ed54a3554877d0cb66d/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f8cec74c4fad7aeb0852cb34c2134b16db05d878ad3946a53337dace7016f4", size = 1117814, upload-time = "2026-08-30T19:53:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/71/a4/8dd6bdc909324d1c39da1c86d65f75512ae049c159952af4cfe8feb5f8d4/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd202d02d129197a5e771f8a11c7d30559927284c23ec3a8bd4f37a7955964d1", size = 1141639, upload-time = "2026-08-30T19:51:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/fa/bd/c13ed6145c360d0770415efd7d5a7e63c29905aeef52ab88004fe7e7f924/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b1e393ab01b71f683ba2a783785871cc6b81a6e41017780c64a5bc0b99759ae", size = 1143334, upload-time = "2026-08-30T19:50:38.045Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/060d79ed8504b54ced9ad16f33d674b1b98a9debe9733c02709d7dd5c71c/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c385c7741893404306f9e5559ab3835432e85e7c153e25f854c502c410bbcbb", size = 1163345, upload-time = "2026-08-30T19:53:06.583Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f7/6e68e2b705f729a6f7b4f41030022b1a5264c5434d3bcd917233d6801c6a/hypothesis-6.167.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:94920ca1fae70c26b0bd3fabbeef9437ffc17a39fe85696fb9a86187d92f6dba", size = 1123029, upload-time = "2026-08-30T19:52:03.134Z" }, - { url = "https://files.pythonhosted.org/packages/a5/c0/d274fe37ed5ecd5ad8ed555edc1f5e2abc8e1c3be3d5404b7edd5cc353a8/hypothesis-6.167.1-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8d90ded2ffdc7e56b5e571993f384b52fade0a7b424e614f999cc2491789970", size = 1154003, upload-time = "2026-08-30T19:50:55.053Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2f/2b5bb386f43fc965eb86fd69fcb2bd62c08cb6d7c6708a40dc39b3b97440/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:40cd5de7dd252942a08480639f5850594b1aca4a463e8a7f15e1fb6c2c3760c1", size = 1293729, upload-time = "2026-08-30T19:52:59.481Z" }, - { url = "https://files.pythonhosted.org/packages/ac/32/22436b072d79011fe81abb933edcd2476057c7b971588c5f3caf07519a88/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b9c33f921ddc7fea93660eca408b25fe755516e22ec7ab21cb9951031f1cd608", size = 1419248, upload-time = "2026-08-30T19:50:52.903Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3e/abf39faaff0f78112112a82316a5c9fe472574480c1ecee526734775b812/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:495989cf0a5ee03f7f9598ee9efeaabf15fd861ec52b5a9d6435849453e17e5d", size = 1274903, upload-time = "2026-08-30T19:52:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/29/83/69b89ed5692ba3aad117facfb9ce099633a22c35acc3d64829b72253ec8c/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:bc73c46ce8ff93b0eb220f2b75adcbd9fcc9112078a74d3522d2532ad8069bad", size = 1294185, upload-time = "2026-08-30T19:50:35.038Z" }, - { url = "https://files.pythonhosted.org/packages/2b/1e/55dfcbe45c72df0a5c5b86a6b7c9365121acab69c2cc060bd55336a48c8f/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36e83e1d7e97aaacbf6cd778e14a841344f848a674b20dfe4fe997546a6a2151", size = 1330013, upload-time = "2026-08-30T19:52:54.841Z" }, - { url = "https://files.pythonhosted.org/packages/3b/a6/0a36cead4ccff58bedb5d1aa2f894f7a580c317424b4fa788c6b22232b2d/hypothesis-6.167.1-cp310-abi3-win32.whl", hash = "sha256:fb4d87454d2459c2ccb541a4c61c92ce13058b91305ed3304695a409a1d886e4", size = 671942, upload-time = "2026-08-30T19:51:32.732Z" }, - { url = "https://files.pythonhosted.org/packages/ec/5b/360285ed42109f5ef48d98ca9ffcf71d1477130c0ff1f1a8d535c8507259/hypothesis-6.167.1-cp310-abi3-win_amd64.whl", hash = "sha256:5e35f98b427bf438a946203426b485dd5b62485f3d5a69a0e0862870a545e518", size = 678637, upload-time = "2026-08-30T19:50:33.573Z" }, - { url = "https://files.pythonhosted.org/packages/79/2d/cc084c1a8bfa296048ec0461f0fa11731abe0097f5c2310c8d39d94c8dc4/hypothesis-6.167.1-cp310-abi3-win_arm64.whl", hash = "sha256:dd6a0808a2eb8b5b1ac06bca4244eee18ed2c0e7b105599e1662203d164317b5", size = 676657, upload-time = "2026-08-30T19:51:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8e/e0f470823bc301a97a8e6156806f6322f5ab99a2f40b6c604261966b3393/hypothesis-6.167.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:64cf8b7ac9a0cc80dad8884f81a2e50f0b79956694927d40e21e0cfa48830b8a", size = 786180, upload-time = "2026-08-30T19:52:09.714Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4d/6e9e430ded5e226f245cd7e55923c7675311fa2115bc4262b5802edfcbb2/hypothesis-6.167.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69b92f037080cefb949c5f9683873abac12283e0dc77201df089369c1ef67e4c", size = 781901, upload-time = "2026-08-30T19:50:42.641Z" }, - { url = "https://files.pythonhosted.org/packages/bd/aa/8df2711daf3ace849045482492b7f178fb55b79201f332477f6eef590b46/hypothesis-6.167.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b334180260de636a0b3017dd96d63709da0c4182c9cb28f52dd6cda78dc933", size = 1118135, upload-time = "2026-08-30T19:51:54.599Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4f/6040b58ffc511013394ba027191dd7e2886c5ae39e0b7872ab15503803fe/hypothesis-6.167.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:769d0242531067e6daf16b6c9894fd9584139884de887023328e3c5658993597", size = 1163947, upload-time = "2026-08-30T19:50:45.908Z" }, - { url = "https://files.pythonhosted.org/packages/ca/66/24aedbae1b56e71308d0aef4415e37c6bb22068c77e95337cdf9d74cd8b1/hypothesis-6.167.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:22b6c3def5446016148523b74ef9da4948bec5ef05865d56c99ba187f4092663", size = 1294290, upload-time = "2026-08-30T19:52:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/a063e3ab97851f4425918f0fcd407d0f426184c26b09f3bda7369e3d38d7/hypothesis-6.167.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbbb6bc17a5a04120a5bfd830335b8b301a22d21d2262804be330c235031b4c5", size = 1330694, upload-time = "2026-08-30T19:51:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f6/6ebb84d532c0d5b36b3bab7b477a167685b2210a0246c7101557bfa4751c/hypothesis-6.167.1-cp310-cp310-win_amd64.whl", hash = "sha256:cdc7e20161f21f14c2d7a057054521db0a8c4bbb647d2e50c90c84f28e4621a0", size = 678586, upload-time = "2026-08-30T19:52:31.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/14/2445b7b1a0c8db61c6812b74606ed7a4f41e3ea0e0c103313e25995b82d5/hypothesis-6.167.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:613bf10e6e490daaa88eb4bf06fb3aacf6572b887e2f9fa5d0bac1be96a18c00", size = 785945, upload-time = "2026-08-30T19:51:19.221Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f6/67926d308a9ab19bb7dfb5118832fa74b508f94871fadbb3370629decbfc/hypothesis-6.167.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f715d912560dd4df3daab4c1fd98c01132eea7ab292f5b9e1d28fc419fe63348", size = 781726, upload-time = "2026-08-30T19:52:57.179Z" }, - { url = "https://files.pythonhosted.org/packages/33/de/22ac0e272530b36ad840170bf661ff89df8648052bb6a37dba520b312f1f/hypothesis-6.167.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdd2fc0232b47910e9621f8c1b5732381438055b03fcc69180e1ef3659b7e70", size = 1117929, upload-time = "2026-08-30T19:51:58.828Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1f/b72b51bac9a7d330bcdda01dc0ab1abe76e1c29ff522effd2d1be4e23702/hypothesis-6.167.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:163e4cebd4b2380ff92f5d36bd04697e82b13973794440694da768521e1e2eab", size = 1163855, upload-time = "2026-08-30T19:52:33.689Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b4/02424328951f0244f7dd3f620775ed22d39dc234e92759f9d2980150252b/hypothesis-6.167.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2de4e67289f86e358732eb16b345f9d1f987c33e44b10d4ffa3ccd715dea9316", size = 1294177, upload-time = "2026-08-30T19:50:44.418Z" }, - { url = "https://files.pythonhosted.org/packages/d8/09/2262d6b362c81066ad451fda48633b2d3ea6cf2d0d673ee6554ad8f86c58/hypothesis-6.167.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fcfd20792f62d65729f850ea50328c1f8b09874d5960b122715b9e6784a7a547", size = 1330267, upload-time = "2026-08-30T19:51:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/70/5d/19e8c02eebfe89592dfd12372032e8868ad40e6d40b38e5ecc4935ce194f/hypothesis-6.167.1-cp311-cp311-win_amd64.whl", hash = "sha256:ebb841d21156039d7da0a41fa9de4ccf468510a4e4d8144f4fe2b3f31239ef3b", size = 678401, upload-time = "2026-08-30T19:51:44.77Z" }, - { url = "https://files.pythonhosted.org/packages/72/82/07987292cfb59c73ce6574e2912c015f678d5aa4d8c0712b78ae4415a535/hypothesis-6.167.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1937ae4e23f7dde6d4202d3d08c2633bcd535a091bdf866b8799abaabcb1e6f0", size = 787050, upload-time = "2026-08-30T19:51:10.782Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e9/0d37051bec44ec433d87da03c9a7fe389b1b58210790c1f166af249bf941/hypothesis-6.167.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1434bbd25d05aaf75c4e6829b4cad8e9931b690f840d92b747b2e6e5af575922", size = 778617, upload-time = "2026-08-30T19:50:31.939Z" }, - { url = "https://files.pythonhosted.org/packages/13/2a/60c18a493215c22c9cfcb4574b381497bd1971eed9fb5f9831b26e73cffb/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3c09428e553b1dd2f9abbc4738377c58bf6d74cb0b8b528cc1dde3a9cdfbe8", size = 1116743, upload-time = "2026-08-30T19:50:49.293Z" }, - { url = "https://files.pythonhosted.org/packages/12/1f/b6796f11d6502e0b1764aec99f2792ca60382b6a45e26bbe163dc5757980/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04f807b85d425a7005e8a24498ca832bf5590f0d306737471d94c842569cecef", size = 1162718, upload-time = "2026-08-30T19:52:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6c/e3cf35474b799e299fa08980b6756f500d730781686916ab65f87cbc0613/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:803c6a98ff66cee4caf03245bfd00e442a907264031b994a3a650dc6e4786f51", size = 1292417, upload-time = "2026-08-30T19:50:56.822Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/875803c80c373a1628f8eb62f215ad23ce7b50ed61f884d6be0838ebea4a/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21a7122ddf072906083e3704fe3961ecbc49d7d30a9b51bcd525d977b3afe65e", size = 1329035, upload-time = "2026-08-30T19:51:46.659Z" }, - { url = "https://files.pythonhosted.org/packages/90/a8/a8daed3796623884471dc0ee8ed63917b1e2b979b4074bcea19a964fcd71/hypothesis-6.167.1-cp312-cp312-win_amd64.whl", hash = "sha256:a2837c60d782eb0b8a910c541264675b9d11486e186af8c82a5e2920b5fe4fe8", size = 675966, upload-time = "2026-08-30T19:51:56.58Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/dca7b211804f60c789aced2792b1e7803ccd8b70b79041cbb92788df5d19/hypothesis-6.167.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6478d19a7887731cc2afaa1ec15f62811c9ceb6fd18e5b7563e0a18399a9528f", size = 786947, upload-time = "2026-08-30T19:51:29.165Z" }, - { url = "https://files.pythonhosted.org/packages/b9/6a/3cffa138492c9e3d5f98f4ff8b467273dc87af6ca3c18084272d106bde10/hypothesis-6.167.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f13167a4b81c93e7e051d1f02790814a6495fb79cacf3fb89560a796a2f7d00", size = 778584, upload-time = "2026-08-30T19:52:25.091Z" }, - { url = "https://files.pythonhosted.org/packages/7a/7d/e8039791aaca3b21557bc520a71cdb88751892f66fd1a0a459b59872e463/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef7dd225f7df7d74d1c5a905592cd8b4cd348e6be639b189a43def8b0b5dd79", size = 1116749, upload-time = "2026-08-30T19:52:38.178Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b8/f9b8d93bd6178870f0daa868ca99915f6d9df1f99dc7291e9ce2743a6dc5/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d7585429f2263d3ceeb3474bae3871024630a7a598e71eeb4b0dcf03e291623", size = 1162599, upload-time = "2026-08-30T19:52:11.72Z" }, - { url = "https://files.pythonhosted.org/packages/a1/0d/53d419094e6f8a7e7377c09de15ac23f842ab698ff07241f7b73e19bd559/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fb9194f450417cf35f66b6c72737cc6b8f21f20567ba4d39822c64f0d1075784", size = 1292230, upload-time = "2026-08-30T19:52:49.732Z" }, - { url = "https://files.pythonhosted.org/packages/5c/df/cf4c482323ae4f06b5326b5bdd89cf17d8232fdb3186c9913e0b19a5fa58/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c63a0d292a5dde3c0fe999892e76d8375a003ca40c0c00d763f3360f91be5b96", size = 1328899, upload-time = "2026-08-30T19:51:05.366Z" }, - { url = "https://files.pythonhosted.org/packages/49/05/780c4b0396491d294fda69a541cb1dedb37fb9eb2e3a696e85fe19064c40/hypothesis-6.167.1-cp313-cp313-win_amd64.whl", hash = "sha256:ff07f98a0b230632bb2836b5dad3e94d85c114ae155a316afd251c58760958ae", size = 675927, upload-time = "2026-08-30T19:50:58.472Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f1/1e602f090dcb7e38655f1f7909482742891332275fc01f241e255cdfa514/hypothesis-6.167.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:fcfc2a78fc1025644f889a74684b3201f4652ce8e6694c2a01af0f100d0348cf", size = 787054, upload-time = "2026-08-30T19:50:40.88Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/cfa930719c7af5a33627abba826a3fa2efa61a5f23e38d4111eace5dfe53/hypothesis-6.167.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:769bdd9aa0af08c063327730ab6dc18b7a23837a2912f2aeaab3912f11a7e3ad", size = 778721, upload-time = "2026-08-30T19:50:36.503Z" }, - { url = "https://files.pythonhosted.org/packages/01/37/4c4bc3d319eac85bfe17515c9786bf49e57181ca8757886110d2cfb13d10/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d75d44bdead6679b6ee9a7c90d10207db865ca0c77c5212103b5ff421379f99e", size = 1116972, upload-time = "2026-08-30T19:51:52.472Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/0d61ceef2739e7b96ea1faa0f3d5aa5917c8156797993bf3acbadfcd7f0a/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742be00d7bb53d10634e6435e5b98f51fcdbe7ed377d473ab7387d9499c87169", size = 1162776, upload-time = "2026-08-30T19:51:09.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/cf/756666ce2262e90fd61fec41a95548cceab94b0669381d8f0387cd89af93/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b6fcdc8d03b37a902262be13112d113bb4ac87edf3b08afb47f3d1210deb038a", size = 1292748, upload-time = "2026-08-30T19:51:17.22Z" }, - { url = "https://files.pythonhosted.org/packages/e8/b4/87eb3c695d6c37fb44f4d49f9faa2033af496e24965658942a1706e22620/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56e7841514276c308c2bb4d033cf01860d0fc8c76e2b79ce748a9f123eaf83b", size = 1329101, upload-time = "2026-08-30T19:51:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3b/87faa4a86533eaaa19037741fb9cdde8647f7ffdf8fd4279828ac9d81f8b/hypothesis-6.167.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bbf4f0cad201d0b8e821e82ad828b2aec99ce6d9967779eecb2cad4d4a93debd", size = 618079, upload-time = "2026-08-30T19:52:43.226Z" }, - { url = "https://files.pythonhosted.org/packages/e0/46/96b7ac9605887447d267b4b3a9ecf61c6caaabf39eef667173b0cc9222b3/hypothesis-6.167.1-cp314-cp314-win_amd64.whl", hash = "sha256:3e04f6001299708b6fd4512267b189c0b029ef1e34500deb4e4c9639023598d7", size = 675812, upload-time = "2026-08-30T19:52:52.298Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f6/0337a91c50ce4be323c3d6aa852fcf08199ffbb1072da09fbe6d602f4dfe/hypothesis-6.167.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:47c99256df28555ecc2aed0e22ca17cd61c63c8c44207a07b4e402cc49661fae", size = 785525, upload-time = "2026-08-30T19:51:03.637Z" }, - { url = "https://files.pythonhosted.org/packages/5a/08/9bb52de855169d31888c7033ee2f94b94138fde021c1af9dbc7ba5e83cd5/hypothesis-6.167.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5d6614e88fd267bbd870e3ec02f8a5387897d2d573626a02f5ac06d81533afa6", size = 777142, upload-time = "2026-08-30T19:52:18.472Z" }, - { url = "https://files.pythonhosted.org/packages/85/79/f1a7e088e13a641357abb9b43d75c116c2a0902711b1a25a203864b96c9b/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27829aa89fe2e47c5c8d13b3ce31e0f53a01a98f76a4f99cfa3369ab35362f33", size = 1115311, upload-time = "2026-08-30T19:52:40.975Z" }, - { url = "https://files.pythonhosted.org/packages/e0/38/e28b1fc20bd3d67d43cf1aab7a15daa2a24ec01d17a153e82fcf38c882f3/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e936777d92ae27393b4a941839bbb43c1f339b5a0e394c7f3730454cdf091b3a", size = 1161238, upload-time = "2026-08-30T19:52:20.501Z" }, - { url = "https://files.pythonhosted.org/packages/5a/de/9b4fc7992166299e0fc5c13c8766919ae57d0fb9eed5319b7a3bad4f2f17/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:971ce0d8a367a37c4690b83a2e7f6ef832fa3543357eb6da0da27ba078e5088a", size = 1290974, upload-time = "2026-08-30T19:51:15.673Z" }, - { url = "https://files.pythonhosted.org/packages/cc/79/ca086eea02588212ab796ee4bd7fe6ed514e10d1a99967e478691608e8d9/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af84ce2416be2a65bc0ea18e64d2dbb9796b7692593b5b2064d60ea1d52ec1e2", size = 1327969, upload-time = "2026-08-30T19:52:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/26/79/1875380fa30e8411553e76b3e9695aca0845f9f265523f20f879bdea2b82/hypothesis-6.167.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3b596efec5bd714588e3bb269544d993c5258c979f3a26f51fadf62c215d0e68", size = 675735, upload-time = "2026-08-30T19:52:22.821Z" }, - { url = "https://files.pythonhosted.org/packages/b0/45/59abecd75e52b9dfb5b3eb991276f54954c44917a1c83d148cfb3580bd39/hypothesis-6.167.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:c25c556d51d55d94988dc0a2c716d471ff19cf9632cd33f5e6db2914d802428a", size = 785097, upload-time = "2026-08-30T19:51:12.528Z" }, - { url = "https://files.pythonhosted.org/packages/01/7c/e6d978dc9564ba70352da60c00f55f6ad7d66d99ecbf336a228978206cb4/hypothesis-6.167.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b57e950f9d5c93ca335bc612e8fa8fb49abb187c3fc9d5e7d9966d52eb27d747", size = 776798, upload-time = "2026-08-30T19:50:47.247Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a4/f8ecedcf96790aab69d750afe3fcbf503229d0bb4e0c32be655385a4fc8c/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0807ae8d399162827fc1c396ab4c697a41921c2a2baacfada439771e9dc2b867", size = 1115116, upload-time = "2026-08-30T19:52:16.029Z" }, - { url = "https://files.pythonhosted.org/packages/7e/97/8bca7c262ac4fcb1ee684c04e4ba75f26541d3a30417e3743d19912d257e/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:546fef39c7aadba74bf3e592585694a71340d1775e9b3274bb3f94106dbde4b7", size = 1137812, upload-time = "2026-08-30T19:51:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/d6/07/9cb4a7fc2aa2b2ad063b446c378f0d7acfa5303e84afd1b1374ba23fd6f3/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17a5618b6a5b84f17c8acb3bce37122647cf7a3e48b660a39d68a773bd627dd", size = 1140384, upload-time = "2026-08-30T19:52:05.264Z" }, - { url = "https://files.pythonhosted.org/packages/1c/18/813bf7efa18f11ce938a0da22a7518a54db46d4a181ebf4cb0a8061c263f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff5ad833480c1e34ae902cb52fc00802b08ac6c87bd22d7e5f04fd925869608", size = 1160569, upload-time = "2026-08-30T19:51:40.099Z" }, - { url = "https://files.pythonhosted.org/packages/52/1d/6658d9294ed33bb17da4acf06fe63b010b205ea1861f6921c38060793255/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:630eb37df80b5bc4ec6f391b13caaecc06942ff5da3aadebacf85f53bbc55757", size = 1120605, upload-time = "2026-08-30T19:53:01.848Z" }, - { url = "https://files.pythonhosted.org/packages/a6/81/a75821c0e879223a2635b8eded84ae874cb6c711b23e9930668008d0b13f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc4e65f7c43b187f7a40964706b5ded1073e0c1839e9fb5e041d7ed973bb65fe", size = 1149479, upload-time = "2026-08-30T19:51:30.972Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f2/c8c3faf4ec796d6dbf36b84662806696434aef38616e5a65b46188c04262/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e849f518cbc4e76ab15f2f1473c60dd3103da8d32399187325ceb84309105976", size = 1290423, upload-time = "2026-08-30T19:50:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e3/e0903abe7e8634cedb5414931452e82daacdd3b8b46d6348bbefcaa45f2a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:5c5a26d4d3dca0c84e01bde41df4cabaa5a373c7393f9eef372d19fe93b07ccd", size = 1415749, upload-time = "2026-08-30T19:51:48.456Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/03f09c1ecac1dfb0f4cd7fcc6dc50d9c6ea8067b295a728e242650bafe32/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4819adbc5911648f6bfaeb574f276add184b4b49f54731dbde46fd71256bb157", size = 1272086, upload-time = "2026-08-30T19:52:29.368Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7c/7a63bfc0bfbaf000f71352c4faac72ff611376330a2ce2e9a1bf4668848a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:3ad7206de9c398c8da5745b69b5ba2ef45100082eeb174656490bc4f262b112c", size = 1291553, upload-time = "2026-08-30T19:52:07.545Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/8065bdab53bc81743ca68fc76ca53fc7531a5b3f01c0de4ba40467955d6a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:96d5e8017a9508f06c8a61a6130cb0d0b4810847ed5c76923cb5cfb9952b31af", size = 1327734, upload-time = "2026-08-30T19:51:42.306Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a0/5c15d480aea3a8e6e5c17c7cb1170171707ac643ffd319473bc194743ad8/hypothesis-6.167.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:a4e4de36a397cba49d949d89cbc26135977c15f9d797caa95317962ceb5b5674", size = 669115, upload-time = "2026-08-30T19:51:14.192Z" }, - { url = "https://files.pythonhosted.org/packages/1b/36/4cf494bc96384189fedb7d3f272580315f2284a9f8a7f6a59796612eb76d/hypothesis-6.167.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:f6fe9c40ab14def363d9e7ab22863fa31652bd5e08f8495b34ff7bd0062b3f8d", size = 675438, upload-time = "2026-08-30T19:51:06.881Z" }, - { url = "https://files.pythonhosted.org/packages/b0/7f/db1a37e5f45be32c0e64f9ed1268eba56aeedcb2ef20d195fa60c6610347/hypothesis-6.167.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:627ce3bd166799a6c0ddcf1351049be5b9a772d5bce436216d42b41a935f42c0", size = 673123, upload-time = "2026-08-30T19:52:13.991Z" }, - { url = "https://files.pythonhosted.org/packages/53/bc/a77ee57eb8fb13f2b5bdfb4a1ea3f32713c50420f0208e84fbe590fad1ad/hypothesis-6.167.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:436027c9a00eb11a2ca3d608147ca2d0d623b4f02878c56a50fc3f3f58c2b41b", size = 786862, upload-time = "2026-08-30T19:51:38.287Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3f/cc9c9120fad719e683914b9204b38f1a30721bc06344f465fc36427cc45e/hypothesis-6.167.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:35e90c121b1518d7428a45e6b0d5c6d06e0ed9eaa567f1106e1f09dae006d6da", size = 782711, upload-time = "2026-08-30T19:50:28.733Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/a48824ba4ad1257904bde4654099851febf0b4c3f018c8174b33d4ba0308/hypothesis-6.167.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c903b4f1c8531736fc7e8e537f47ef509756b731a32a5e5e7014e5291343acb", size = 1118685, upload-time = "2026-08-30T19:52:01.045Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f3/c6bcfb38c4b4cd22494902f5368b5815425f03eeb5159741c7d910a69af5/hypothesis-6.167.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ca252991fdbe2ff5c611a1cc4d972d4e009eb45292c7802faa2190f995dc50", size = 1165389, upload-time = "2026-08-30T19:50:39.44Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f6/d1d19d115a4c0aa35c87b9e5d570b0a7c9f42f816087849cf29c58664426/hypothesis-6.167.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c91f6f2f15b8bc6e474b824f931247c39711231e7b1b2f68277726e0ae1c728", size = 679392, upload-time = "2026-08-30T19:51:00.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f8/8b2cc9ae7b439538f6f2d32a92892340b6343a6b04d171c516666901dedc/hypothesis-6.168.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2", size = 791358, upload-time = "2026-09-08T18:47:37.076Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/4d7d897310cde5085779fb96feadb8529d98cb8e51ed7b24f7da9b6c6bdc/hypothesis-6.168.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099", size = 787081, upload-time = "2026-09-08T18:47:16.227Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/9d52066d363faba7f3ac20ee60a3c696a475feb2c477d235d0d649d41cc1/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4", size = 1123850, upload-time = "2026-09-08T18:48:11.504Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/aa46d76fa7df43caf2c372e394fda84ce1dc08421814f8674a6b9295e2ea/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f", size = 1147685, upload-time = "2026-09-08T18:46:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/c2/78ed8c8d5aa37e4baae2a4b3e29687ee3d9b7f1a5e56ea5ea5ec7ec71ecb/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1", size = 1149294, upload-time = "2026-09-08T18:47:04.757Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/d57440f19e9de70e85359cf179ce786f309ee17609bd3c5a0113272875c0/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213", size = 1169729, upload-time = "2026-09-08T18:47:44.056Z" }, + { url = "https://files.pythonhosted.org/packages/a1/86/dc74410a186990bb22c2a3eea0e77804f2eb0f300860b46d7d8948073674/hypothesis-6.168.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131", size = 1129182, upload-time = "2026-09-08T18:46:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/be048fc4f6dac831625e155bdf11e4233caf46bb54030391d6fba8e19449/hypothesis-6.168.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149", size = 1160180, upload-time = "2026-09-08T18:47:26.459Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4a/15a34498a5f08720fbbdbbe4668a8050fe4e17c16c9eeb6f56a017f2fa6b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d", size = 1299711, upload-time = "2026-09-08T18:47:28.34Z" }, + { url = "https://files.pythonhosted.org/packages/77/09/5354e0dae302ab98c4f0046b7e8699c2186ae4349397bda5d5c852bda68b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517", size = 1425341, upload-time = "2026-09-08T18:46:56.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/7c/3c0e1f59043ff128c70a51d298d3d6b5973525c357a1e1d0542dbc05ac90/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349", size = 1281063, upload-time = "2026-09-08T18:46:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/db884db9a7d42ae6b72a00c13c725940b638dfb263e618b22af59d5dfa2f/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b", size = 1300247, upload-time = "2026-09-08T18:47:02.949Z" }, + { url = "https://files.pythonhosted.org/packages/99/8a/4ee9769e1d48676efb6a78a130f82e0d52d3f87b2055294102272a08615c/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377", size = 1336084, upload-time = "2026-09-08T18:48:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/61/17/d4ed11bc99d205d6d2651a0f1a4874f377150f836b5a0849bd511d68a2eb/hypothesis-6.168.0-cp310-abi3-win32.whl", hash = "sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29", size = 677989, upload-time = "2026-09-08T18:47:24.722Z" }, + { url = "https://files.pythonhosted.org/packages/77/51/abf1fde7b8afab87db30afb73b3847e62440551d146472111cabeba2fe00/hypothesis-6.168.0-cp310-abi3-win_amd64.whl", hash = "sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b", size = 684692, upload-time = "2026-09-08T18:46:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/12/e2/64d79aed47a95186ce9edcef60eb4684870c72542da3b7027c2483d8ee8c/hypothesis-6.168.0-cp310-abi3-win_arm64.whl", hash = "sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc", size = 682709, upload-time = "2026-09-08T18:48:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/bfacadcc6cd4bd179d1cd2f6c85f580136019b38af7db71e3159ebb3ce33/hypothesis-6.168.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5", size = 792193, upload-time = "2026-09-08T18:47:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/4abab4114035f54438e3b07bdab3f68fd8769fe0fb84f8ffd6656eaa58fd/hypothesis-6.168.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc", size = 788121, upload-time = "2026-09-08T18:47:09.398Z" }, + { url = "https://files.pythonhosted.org/packages/85/d3/c54242439dcb59c7e3274a2952188bebbc605be397c3a6ff027fc81e1726/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3", size = 1124222, upload-time = "2026-09-08T18:46:28.345Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/1c9f54be7040fe7d94e4cd5d9343da5a36240bee2fa856f44109f9622251/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67", size = 1170346, upload-time = "2026-09-08T18:46:52.784Z" }, + { url = "https://files.pythonhosted.org/packages/af/d4/f6b1557bd3cf2976d56465fcbc483214bc6e7f1aa7e4db9f8c5f776d547a/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0", size = 1300267, upload-time = "2026-09-08T18:46:45.644Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ad/a345588acc038deb09a43610328004d31fe5a53289627e03b0199978dc45/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f", size = 1336724, upload-time = "2026-09-08T18:46:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/97/e9/c5e380fd29fd552ff442bcfb1090926287e2911d40212ae0602a0736347b/hypothesis-6.168.0-cp310-cp310-win_amd64.whl", hash = "sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236", size = 684644, upload-time = "2026-09-08T18:47:45.814Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7a/7a28d9afd52c9a89c4d8e3170c92fe1ffbd4a4ce3f905ca15fa7ddaaa581/hypothesis-6.168.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a", size = 792045, upload-time = "2026-09-08T18:46:29.612Z" }, + { url = "https://files.pythonhosted.org/packages/0a/40/e2d6fe12b54abbc8b802b234ec4974d0a420dde4098f57a324e9c791ac27/hypothesis-6.168.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9", size = 787932, upload-time = "2026-09-08T18:47:11.145Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/cc78e05f3248949c6d9ecb489d0e422247d11b08533ab02e177a10a01610/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843", size = 1123994, upload-time = "2026-09-08T18:47:12.647Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e3/7d483a79e2ed9a6c868cac8783b3c6c2bb82303f7db364c9da9a406df10d/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063", size = 1170271, upload-time = "2026-09-08T18:47:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/bffaf5b5e6b4566aa40d8a4ae25d8d84b47dfe0d9e771f454e4974cdb428/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04", size = 1300162, upload-time = "2026-09-08T18:46:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/6c30a00fc2f5c58546e858259cf2261c6a3ef6a060c373e00d00311267be/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f", size = 1336294, upload-time = "2026-09-08T18:46:37.324Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/501910e5a6a9c245f2394fda016376f6c7e209c5dca8c056eaece64ad10e/hypothesis-6.168.0-cp311-cp311-win_amd64.whl", hash = "sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2", size = 684464, upload-time = "2026-09-08T18:47:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/0035896c101f0484c364353f8ee30175eef8936b49171618677287fdd85d/hypothesis-6.168.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda", size = 793115, upload-time = "2026-09-08T18:48:21.352Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/938173e27df771cc6e92bc47f117a1b1be4a88fc6dc214f7fc65f9c7ad93/hypothesis-6.168.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79", size = 784634, upload-time = "2026-09-08T18:47:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/0b2c6ac07d519131f97712f2533750ffe9b2490eec8f518ef3a5ed2dd514/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7", size = 1122855, upload-time = "2026-09-08T18:46:21.967Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/dde930fe43171cab37572bd993d70c2a271f240f428f8ccd64ec4c2d661b/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4", size = 1168932, upload-time = "2026-09-08T18:46:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5d/92b83c3d06194ec626e92723d0b0f70221ebf42d7cb355ed36929df6d735/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d", size = 1298566, upload-time = "2026-09-08T18:48:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/9c/67/52de8bf3446e3d2d555b812d96673b31bb213b5b9d5804a666e5d0bba76e/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d", size = 1335074, upload-time = "2026-09-08T18:48:01.561Z" }, + { url = "https://files.pythonhosted.org/packages/14/fd/e592773c1c0ce55e35d26ec55f75546bf1fd72ef5a5c520ed685969b40cb/hypothesis-6.168.0-cp312-cp312-win_amd64.whl", hash = "sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68", size = 682026, upload-time = "2026-09-08T18:47:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/39bb8fcccfbafd10fcc777d583c6ebad5d2148e7d743cb350562f28e974f/hypothesis-6.168.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28", size = 793050, upload-time = "2026-09-08T18:47:56.045Z" }, + { url = "https://files.pythonhosted.org/packages/f7/dd/00fd32e8ec470535e0065cb8d6e175f9fc54b4d3a6f1269f6c487f8bd79d/hypothesis-6.168.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20", size = 784558, upload-time = "2026-09-08T18:46:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4d/553c47093f68bdbac0438e16c024ce97649b5804dc6972ef86b9bd2db8a1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475", size = 1122841, upload-time = "2026-09-08T18:48:09.374Z" }, + { url = "https://files.pythonhosted.org/packages/43/d6/0b5940aa75e617c8fd12200bae24d1b71347362514e8210735c581d4d3d1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0", size = 1168825, upload-time = "2026-09-08T18:47:33.702Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/800de1231b2869b51409bbf85799d6f1bf49a00e0afff0aabc097aa8f1b7/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669", size = 1298433, upload-time = "2026-09-08T18:46:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/9907667a30c1dbabbc44a09b4c25a0f575570937fcbbada924ae3a1dbf2a/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c", size = 1334988, upload-time = "2026-09-08T18:48:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/7bf214e703fff532ed47cb52ab93ce4b7ea41e4e7084593fa02b740828b7/hypothesis-6.168.0-cp313-cp313-win_amd64.whl", hash = "sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d", size = 681990, upload-time = "2026-09-08T18:47:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c1/64b36b250b1f66abb6ce8c81775d3373d149bc89cbea477ed71b57cf7d1b/hypothesis-6.168.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc", size = 793101, upload-time = "2026-09-08T18:46:54.067Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c4/494e42304b15f4ec649d36bbc3fc01cef1b63405cf30d4087ae07d048172/hypothesis-6.168.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8", size = 784632, upload-time = "2026-09-08T18:48:31.551Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/90d874cb1d749f505749c9908f34e803b97b03457797d5893354980558bc/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93", size = 1123101, upload-time = "2026-09-08T18:48:17.362Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/ec893d0e5f4bcdd0121a8a4280f4e8aba3b4cdae01411f3236016ecd1f81/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7", size = 1168962, upload-time = "2026-09-08T18:46:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/16ec2bddbaed461725d9aa5a80b43f1905ea50a08f689ec46f8966bb4f0f/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9", size = 1298932, upload-time = "2026-09-08T18:47:40.632Z" }, + { url = "https://files.pythonhosted.org/packages/8f/12/7c2fe2706d092f12bd7b3e8565e1ca5d0c24b853751f2f970768086dbdeb/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6", size = 1335196, upload-time = "2026-09-08T18:47:57.775Z" }, + { url = "https://files.pythonhosted.org/packages/20/35/59f7ca2414ca39408d13f66a344affe0ffc64748dc01d8a1ca910009cdcb/hypothesis-6.168.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27", size = 624102, upload-time = "2026-09-08T18:47:38.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/dcd9335ace916ffea40f2cb04ba4122094c2f7b928f3a73fc7d452ce5b71/hypothesis-6.168.0-cp314-cp314-win_amd64.whl", hash = "sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193", size = 681871, upload-time = "2026-09-08T18:47:59.697Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/bc50b0b91e40744b7caa56b8add85cef432f85b4d00108409e8eb17af830/hypothesis-6.168.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1", size = 791695, upload-time = "2026-09-08T18:47:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/4b/53/fc7537d50ff008dc5ea8598764935f93dd07bcedaf23ee4e635bdf7055f4/hypothesis-6.168.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd", size = 783239, upload-time = "2026-09-08T18:47:47.43Z" }, + { url = "https://files.pythonhosted.org/packages/71/2a/c7aac2efc06713f704d7e354755aff4a11608b9fe93d973ead374f3b81a3/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507", size = 1121412, upload-time = "2026-09-08T18:47:07.927Z" }, + { url = "https://files.pythonhosted.org/packages/60/e2/668ab29e5096af682b17b8491f5427d7c5f17c1b991daa5577bde80c29ca/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4", size = 1167570, upload-time = "2026-09-08T18:47:17.94Z" }, + { url = "https://files.pythonhosted.org/packages/3a/17/c64635e4c988b5fa3d3b8be322e19e2fe4c731fa0ca074ca852aa70debea/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8", size = 1297118, upload-time = "2026-09-08T18:48:13.407Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/c7a0d9a06bf5c3279386dd53161081a57b98c6faf60fbbf64d046315e9e6/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571", size = 1334068, upload-time = "2026-09-08T18:48:15.462Z" }, + { url = "https://files.pythonhosted.org/packages/32/99/11a393a20a867e9b978308323d45022e96f5cd2edf391e4d9a65fb4e2cf7/hypothesis-6.168.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8", size = 681795, upload-time = "2026-09-08T18:47:14.415Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/5adae1bf1d4877aca2e8c8e007e57077237e9ac3765e430ffda490b17e19/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128", size = 791056, upload-time = "2026-09-08T18:48:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/f2/93/b1b2770b87591db5cf564b9aa0265cf21e9207d28645e0abdeb8df63225b/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795", size = 782980, upload-time = "2026-09-08T18:48:34.087Z" }, + { url = "https://files.pythonhosted.org/packages/de/bd/673171c1d2423379a7d4a0f9f009cca735a422d0c4a4ac6422d1d1736cab/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441", size = 1120964, upload-time = "2026-09-08T18:46:59.92Z" }, + { url = "https://files.pythonhosted.org/packages/7d/00/33a9bd941b22a4fd8a8c805b1563e0db17efc020422f5bd15bdd0fdf258f/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63", size = 1143869, upload-time = "2026-09-08T18:46:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c1/963460976f41721eff8f67f30d31059ea407cc1b41c737c8859aabf37197/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782", size = 1146453, upload-time = "2026-09-08T18:46:25.757Z" }, + { url = "https://files.pythonhosted.org/packages/ce/53/09db238098ad66f4e6d2fe883f26c270c2595b90e21c8f969d4cb21cad7a/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc", size = 1166918, upload-time = "2026-09-08T18:48:23.436Z" }, + { url = "https://files.pythonhosted.org/packages/b9/31/e1b7b452c8a6166e445ba2ad80a864f6a9eee0fe4c8cecdb9af5c1ee0aa5/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e", size = 1126637, upload-time = "2026-09-08T18:47:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/2b/4eceed248afb46fb6b2df21cf2239362de5b25d295c2dc67a82ec8657d5e/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c", size = 1155682, upload-time = "2026-09-08T18:47:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3e/f3414cda4f325004d5774485e8983b7d1b99e4b91013f013dd088fd778cf/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9", size = 1296464, upload-time = "2026-09-08T18:46:34.833Z" }, + { url = "https://files.pythonhosted.org/packages/aa/40/ca79cf96545e1f172b36b8df56bfeb02b61f351f283026f9a57c4631e368/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad", size = 1421853, upload-time = "2026-09-08T18:47:23.083Z" }, + { url = "https://files.pythonhosted.org/packages/77/bc/657d5386740c1f4ac518ff05e4c5b132f1e6df2e7c865ba8fda4279adfa5/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f", size = 1278216, upload-time = "2026-09-08T18:46:30.871Z" }, + { url = "https://files.pythonhosted.org/packages/51/54/2328cdb70489a36634534478d9b238594a269d8bec4630ea0e6897048347/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09", size = 1297593, upload-time = "2026-09-08T18:46:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ca/d803fa57e3ff7f460f6e262d2b74822cf143343d378501fd3da601b12040/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887", size = 1333785, upload-time = "2026-09-08T18:48:25.434Z" }, + { url = "https://files.pythonhosted.org/packages/86/8f/b9799ae6ba6074f151db2c63f9f6f12d844512821ffaba1a3672d7e59f07/hypothesis-6.168.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780", size = 675173, upload-time = "2026-09-08T18:47:52.601Z" }, + { url = "https://files.pythonhosted.org/packages/61/54/14c3e277b451ff24128ecc2673cac59dd7e535bce1a433c466912fd682e1/hypothesis-6.168.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef", size = 681491, upload-time = "2026-09-08T18:46:51.437Z" }, + { url = "https://files.pythonhosted.org/packages/99/f3/827e4a48ffee7e40244b0bf064ba47c2171e053ab7edf1cf770105e23401/hypothesis-6.168.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a", size = 679197, upload-time = "2026-09-08T18:47:32.112Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/7f15e1b10d13e266b0f09cf3117dd2206104a1f6ac3b3baf6ec4c43b7c2e/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76", size = 792946, upload-time = "2026-09-08T18:47:35.438Z" }, + { url = "https://files.pythonhosted.org/packages/10/fb/32487bdcf68b3805ec5efcb7f92fb002b573ad3f90c252d2f2913c5d3124/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01", size = 788783, upload-time = "2026-09-08T18:46:42.862Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/1aa42e0069ebc68d29d980d47420af324fa002494e01c1c0321c34f609ba/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab", size = 1124762, upload-time = "2026-09-08T18:48:07.512Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4e/99509045ed55aaa05d8408663051a93168d1a8a5bfc132d92ec1584ce47a/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7", size = 1171748, upload-time = "2026-09-08T18:46:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c9/96e57dfd89913322b5180b031b9e36a29bfe67842108b19904466476cd24/hypothesis-6.168.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c", size = 685447, upload-time = "2026-09-08T18:46:35.97Z" }, ] [[package]] @@ -362,15 +362,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] From b7708701b837e0780f393af14d95ef4e93834c3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:40:07 +0200 Subject: [PATCH 075/322] chore: Bump the all-actions group with 2 updates (#4974) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 36add34cf8..e07b9a9d21 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup-cli@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index b8248fb542..5fa84fbe48 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@be41add0f204de343293628bb99a72a3894cfd7a # v0.88.4 + uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 9193c17c02cf4d1d4e9102e145e0e0e2f66f1ab2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:40:50 +0200 Subject: [PATCH 076/322] chore: Bump the all-npm group with 2 updates (#4972) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 224 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bae0e04a1..817dcb1713 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.4.1", + "@types/node": "^26.5.1", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.1", + "rollup": "^4.63.2", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", - "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", + "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", - "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", + "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", - "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", + "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", - "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", + "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", - "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", + "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", - "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", + "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", - "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", + "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", - "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", + "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", - "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", + "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", - "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", + "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", - "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", + "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", - "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", + "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", - "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", + "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", - "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", + "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", - "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", + "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", - "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", + "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", - "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", + "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", - "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", + "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", - "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", + "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", - "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", + "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", - "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", + "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", - "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", + "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", - "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", + "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", - "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", + "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", - "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", + "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", "cpu": [ "x64" ], @@ -482,12 +482,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", - "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", + "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/unist": { @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", - "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "version": "4.63.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz", + "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.1", - "@rollup/rollup-android-arm64": "4.63.1", - "@rollup/rollup-darwin-arm64": "4.63.1", - "@rollup/rollup-darwin-x64": "4.63.1", - "@rollup/rollup-freebsd-arm64": "4.63.1", - "@rollup/rollup-freebsd-x64": "4.63.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", - "@rollup/rollup-linux-arm-musleabihf": "4.63.1", - "@rollup/rollup-linux-arm64-gnu": "4.63.1", - "@rollup/rollup-linux-arm64-musl": "4.63.1", - "@rollup/rollup-linux-loong64-gnu": "4.63.1", - "@rollup/rollup-linux-loong64-musl": "4.63.1", - "@rollup/rollup-linux-ppc64-gnu": "4.63.1", - "@rollup/rollup-linux-ppc64-musl": "4.63.1", - "@rollup/rollup-linux-riscv64-gnu": "4.63.1", - "@rollup/rollup-linux-riscv64-musl": "4.63.1", - "@rollup/rollup-linux-s390x-gnu": "4.63.1", - "@rollup/rollup-linux-x64-gnu": "4.63.1", - "@rollup/rollup-linux-x64-musl": "4.63.1", - "@rollup/rollup-openbsd-x64": "4.63.1", - "@rollup/rollup-openharmony-arm64": "4.63.1", - "@rollup/rollup-win32-arm64-msvc": "4.63.1", - "@rollup/rollup-win32-ia32-msvc": "4.63.1", - "@rollup/rollup-win32-x64-gnu": "4.63.1", - "@rollup/rollup-win32-x64-msvc": "4.63.1", + "@rollup/rollup-android-arm-eabi": "4.63.2", + "@rollup/rollup-android-arm64": "4.63.2", + "@rollup/rollup-darwin-arm64": "4.63.2", + "@rollup/rollup-darwin-x64": "4.63.2", + "@rollup/rollup-freebsd-arm64": "4.63.2", + "@rollup/rollup-freebsd-x64": "4.63.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", + "@rollup/rollup-linux-arm-musleabihf": "4.63.2", + "@rollup/rollup-linux-arm64-gnu": "4.63.2", + "@rollup/rollup-linux-arm64-musl": "4.63.2", + "@rollup/rollup-linux-loong64-gnu": "4.63.2", + "@rollup/rollup-linux-loong64-musl": "4.63.2", + "@rollup/rollup-linux-ppc64-gnu": "4.63.2", + "@rollup/rollup-linux-ppc64-musl": "4.63.2", + "@rollup/rollup-linux-riscv64-gnu": "4.63.2", + "@rollup/rollup-linux-riscv64-musl": "4.63.2", + "@rollup/rollup-linux-s390x-gnu": "4.63.2", + "@rollup/rollup-linux-x64-gnu": "4.63.2", + "@rollup/rollup-linux-x64-musl": "4.63.2", + "@rollup/rollup-openbsd-x64": "4.63.2", + "@rollup/rollup-openharmony-arm64": "4.63.2", + "@rollup/rollup-win32-arm64-msvc": "4.63.2", + "@rollup/rollup-win32-ia32-msvc": "4.63.2", + "@rollup/rollup-win32-x64-gnu": "4.63.2", + "@rollup/rollup-win32-x64-msvc": "4.63.2", "fsevents": "~2.3.2" } }, @@ -2751,9 +2751,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "license": "MIT" }, "node_modules/url-template": { diff --git a/package.json b/package.json index 9949a65b30..18dcd7ea05 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.4.1", + "@types/node": "^26.5.1", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.1", + "rollup": "^4.63.2", "terser": "^5.51.2", "typescript": "^7.0.2" }, From e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 18 Sep 2026 19:37:50 +0200 Subject: [PATCH 077/322] fix(python): keep the payload type when reading an erased union field (#4981) --- src/Fable.Transforms/FSharp2Fable.fs | 7 +++++-- tests/Beam/InteropTests.fs | 20 ++++++++++++++++++++ tests/Js/Main/UnionTypeTests.fs | 15 +++++++++++++++ tests/Python/TestUnionType.fs | 16 ++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index ddbabedd80..db18d095fb 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1316,10 +1316,13 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | ErasedUnionCase -> let index = unionCase.Fields |> Seq.findIndex (fun x -> x.Name = field.Name) - return Fable.Get(unionExpr, Fable.TupleIndex(index), makeType ctx.GenericArgs unionType, r) + return Fable.Get(unionExpr, Fable.TupleIndex(index), fieldType, r) | ErasedUnion _ -> if unionCase.Fields.Count = 1 then - return unionExpr + if unionExpr.Type = fieldType then + return unionExpr + else + return Fable.TypeCast(unionExpr, fieldType) else let index = unionCase.Fields |> Seq.findIndex (fun x -> x.Name = field.Name) diff --git a/tests/Beam/InteropTests.fs b/tests/Beam/InteropTests.fs index 7c4ac25c19..1c0d4282bf 100644 --- a/tests/Beam/InteropTests.fs +++ b/tests/Beam/InteropTests.fs @@ -104,6 +104,13 @@ type ErasedUnion = type ErasedUnionWithMultipleFields = | ErasedUnionWithMultipleFields of string * int +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + // ============================================================ // StringEnum tests // ============================================================ @@ -408,6 +415,19 @@ let ``test Erased types can have members`` () = () #endif +[] +let ``test Erased union payload fields can be read`` () = +#if FABLE_COMPILER + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 +#else + () +#endif + [] let ``test Erased unions with multiple fields work`` () = #if FABLE_COMPILER diff --git a/tests/Js/Main/UnionTypeTests.fs b/tests/Js/Main/UnionTypeTests.fs index 3d714753b4..3c6a311449 100644 --- a/tests/Js/Main/UnionTypeTests.fs +++ b/tests/Js/Main/UnionTypeTests.fs @@ -81,6 +81,13 @@ open Fable.Core.JsInterop #endif type DU = Int of int | Str of string +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + type T1 = T1 type T2 = T2 type T3 = T3 @@ -213,6 +220,14 @@ let tests = "HELLO" |> Wrapper |> U3.Case3 |> toString |> equal "OLLEH" #endif + testCase "Erased union payload fields can be read" <| fun () -> + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 + testCase "Equality works in filter" <| fun () -> let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index f9b3d3f7bc..a9775b681c 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -82,6 +82,13 @@ open Fable.Core #endif type DU = Int of int | Str of string +type ErasedPayload = { Description: string; Membership: int } + +[] +type ErasedWrapper = + | ErasedSuccess of ErasedPayload + | ErasedFailure of string + [] let ``test Union cases matches with no arguments can be generated`` () = let x = Male @@ -200,6 +207,15 @@ let ``test Erased union type testing works`` () = "HELLO" |> Wrapper |> U3.Case3 |> toString |> equal "OLLEH" #endif +[] +let ``test Erased union payload fields can be read`` () = + let payload = { Description = "a"; Membership = 2 } + + match ErasedSuccess payload with + | ErasedSuccess details -> details.Membership + | ErasedFailure _ -> -1 + |> equal 2 + [] let ``test Equality works in filter`` () = let original = [| { Name = "1"; Case = MyUnion3.Case1 } ; { Name = "2"; Case = MyUnion3.Case1 }; { Name = "3"; Case = MyUnion3.Case2 }; { Name = "4"; Case = MyUnion3.Case3 } |] From b5a337d0f71b2b1ae91516b8be8a65c7cadc9953 Mon Sep 17 00:00:00 2001 From: Leo <45302927+Tseing@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:50:03 +0800 Subject: [PATCH 078/322] feat(dart): add `Async` runtime and `Future` interop for dart (#4978) Co-authored-by: Mangel Maxime --- src/Fable.Core/Fable.Core.Dart.fs | 6 + src/Fable.Transforms/Dart/Fable2Dart.fs | 13 + src/Fable.Transforms/Dart/Replacements.fs | 13 + src/fable-library-dart/Async.dart | 323 +++++++ src/fable-library-dart/AsyncBuilder.dart | 334 +++++++ .../Fable.Library.Dart.fsproj | 2 + tests/Dart/main.dart | 2 + tests/Dart/src/AsyncTests.fs | 838 ++++++++++++++++++ tests/Dart/src/Fable.Tests.Dart.fsproj | 1 + tests/Dart/src/Tests.Util.fs | 3 + 10 files changed, 1535 insertions(+) create mode 100644 src/fable-library-dart/Async.dart create mode 100644 src/fable-library-dart/AsyncBuilder.dart create mode 100644 tests/Dart/src/AsyncTests.fs diff --git a/src/Fable.Core/Fable.Core.Dart.fs b/src/Fable.Core/Fable.Core.Dart.fs index 79873d3880..72bcfbc7ce 100644 --- a/src/Fable.Core/Fable.Core.Dart.fs +++ b/src/Fable.Core/Fable.Core.Dart.fs @@ -27,6 +27,12 @@ type Future<'T> = interface end [] type Stream<'T> = interface end +type Async with + static member AwaitFuture(future: Future<'T>) : Async<'T> = nativeOnly + + static member StartAsFuture(workflow: Async<'T>, ?token: System.Threading.CancellationToken) : Future<'T> = + nativeOnly + // [] [] let print (item: obj) : unit = nativeOnly diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 15943a8a5d..8f26f510b8 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -2158,6 +2158,19 @@ module Util = bindings @ body, captureExpr | Fable.Sequential exprs -> + // Fable appends a unit value after a throw, which Dart cannot type + let exprs = + match + exprs + |> List.tryFindIndex (fun e -> + match e with + | Fable.Extended(Fable.Throw _, _) -> true + | _ -> false + ) + with + | Some i -> List.truncate (i + 1) exprs + | None -> exprs + let exprs, lastExpr = List.splitLast exprs let statements1 = exprs |> List.collect (transform com ctx Ignore >> fst) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index f9f0a6dcaa..94702f7a64 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -632,6 +632,13 @@ let tryEntityIdent (com: Compiler) entFullName = match entFullName with | "Fable.Core.Dart.Future`1" -> makeIdentExpr "Future" |> Some | "Fable.Core.Dart.Stream`1" -> makeIdentExpr "Stream" |> Some + | "Microsoft.FSharp.Control.FSharpAsync`1" -> makeImportLib com MetaType "Async" "AsyncBuilder" |> Some + | "System.Threading.CancellationToken" + | "System.Threading.CancellationTokenSource" -> + makeImportLib com MetaType "CancellationToken" "AsyncBuilder" |> Some + | "System.Threading.CancellationTokenRegistration" -> makeImportLib com MetaType "IDisposable" "Types" |> Some + | "System.OperationCanceledException" -> + makeImportLib com MetaType "OperationCanceledException" "AsyncBuilder" |> Some | BuiltinDefinition BclDateOnly | BuiltinDefinition BclDateTime | BuiltinDefinition BclDateTimeOffset -> makeIdentExpr "DateTime" |> Some @@ -715,6 +722,8 @@ let tryCoreOp com r t coreModule coreMember args = let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.DeclaringEntityFullName, i.CompiledName with | _, UniversalFableCoreHelpers com ctx r t i args error expr -> Some expr + | _, "Async.AwaitFuture.Static" -> Helper.LibCall(com, "Async", "awaitFuture", t, args, ?loc = r) |> Some + | _, "Async.StartAsFuture.Static" -> Helper.LibCall(com, "Async", "startAsFuture", t, args, ?loc = r) |> Some | "Fable.Core.Reflection", meth -> Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some | "Fable.Core.Compiler", meth -> match meth with @@ -2614,6 +2623,9 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.DeclaringEntityFullName with | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args + | "System.OperationCanceledException" -> + let e = makeImportLib com Any "OperationCanceledException" "AsyncBuilder" + Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | _ -> let e = makeImportLib com Any "ExceptionBase" "Types" Helper.ConstructorCall(e, t, args, ?loc = r) |> Some @@ -4030,6 +4042,7 @@ let private replacedModules = "System.Random", random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels + "System.Threading.CancellationTokenRegistration", disposables "System.Threading.Monitor", monitor "System.Activator", activator "System.Text.Encoding", encoding diff --git a/src/fable-library-dart/Async.dart b/src/fable-library-dart/Async.dart new file mode 100644 index 0000000000..ea4ce13c0a --- /dev/null +++ b/src/fable-library-dart/Async.dart @@ -0,0 +1,323 @@ +import 'dart:async' as dart_async; + +import 'AsyncBuilder.dart' as async_builder; +import 'Choice.dart' as choice; +import 'System.dart' as system; +import 'Types.dart' as types; + +void _emptyContinuation(T _value) {} + +async_builder.Async makeAsync(async_builder.Async body) { + return body; +} + +void invoke( + async_builder.Async computation, + async_builder.IAsyncContext ctx, +) { + computation(ctx); +} + +void callThenInvoke( + async_builder.IAsyncContext ctx, + U result1, + Function part2, +) { + async_builder.invokeBinderAsAsync(part2, result1)(ctx); +} + +void bind( + async_builder.IAsyncContext ctx, + async_builder.Async part1, + Function part2, +) { + async_builder.protectedBind(part1, part2)(ctx); +} + +Duration _cancellationDelay(Object? value) { + if (value is Duration) { + return value; + } + + if (value is num) { + return Duration(milliseconds: value.toInt()); + } + + throw ArgumentError.value( + value, + 'value', + 'Expected Duration or milliseconds.', + ); +} + +async_builder.CancellationToken createCancellationToken([Object? arg]) { + final token = async_builder.CancellationToken(arg is bool ? arg : false); + + if (arg is Duration || arg is num) { + token.cancelAfter(_cancellationDelay(arg)); + } + + return token; +} + +void cancel(async_builder.CancellationToken token) { + token.cancel(); +} + +void cancelAfter(async_builder.CancellationToken token, Object delay) { + token.cancelAfter(_cancellationDelay(delay)); +} + +bool isCancellationRequested(async_builder.CancellationToken? token) { + return token?.isCancelled ?? false; +} + +void throwIfCancellationRequested(async_builder.CancellationToken? token) { + if (token != null && token.isCancelled) { + throw async_builder.OperationCanceledException(); + } +} + +async_builder.Async cancellationToken() { + return async_builder.protectedCont((ctx) => ctx.onSuccess(ctx.cancelToken)); +} + +final defaultCancellationToken = async_builder.CancellationToken(); + +async_builder.Async> catchAsync( + async_builder.Async work, +) { + return async_builder.protectedCont>((ctx) { + work( + async_builder.IAsyncContext( + onSuccess: (value) { + ctx.onSuccess(choice.Choice_makeChoice1Of2(value)); + }, + onError: (error) { + ctx.onSuccess(choice.Choice_makeChoice2Of2(error)); + }, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); +} + +async_builder.Async fromContinuations( + void Function(async_builder.Continuations) function, +) { + return async_builder.protectedCont((ctx) { + function( + types.Tuple3< + async_builder.Continuation, + async_builder.Continuation, + async_builder.Continuation + >(ctx.onSuccess, ctx.onError, ctx.onCancel), + ); + }); +} + +async_builder.Async> startChild( + async_builder.Async computation, [ + types.Some? millisecondsTimeout, +]) { + return async_builder.protectedCont>((ctx) { + final future = startAsFuture(computation, types.Some(ctx.cancelToken)); + + var futureToRun = future; + + final timeout = millisecondsTimeout?.value; + + if (timeout != null && timeout > 0) { + futureToRun = future.timeout( + Duration(milliseconds: timeout), + onTimeout: () => throw system.TimeoutException_$ctor(), + ); + } + + // In F# an unobserved child's failure is not propagated. + futureToRun.ignore(); + + async_builder.protectedReturn(awaitFuture(futureToRun))(ctx); + }); +} + +async_builder.Async> parallel( + Iterable> computations, +) { + return async_builder.protectedCont>((ctx) { + final futures = computations + .map((w) => startAsFuture(w, types.Some(ctx.cancelToken))) + .toList(growable: false); + + awaitFuture>(dart_async.Future.wait(futures, eagerError: true))( + ctx, + ); + }); +} + +async_builder.Async> sequential( + Iterable> computations, +) { + dart_async.Future> run( + async_builder.CancellationToken cancelToken, + ) async { + final results = []; + + for (final computation in computations) { + results.add(await startAsFuture(computation, types.Some(cancelToken))); + } + + return results; + } + + return async_builder.protectedCont>((ctx) { + awaitFuture>(run(ctx.cancelToken))(ctx); + }); +} + +async_builder.Async ignore(async_builder.Async computation) { + return async_builder.protectedBind( + computation, + (_) => async_builder.protectedReturn(null), + ); +} + +void start( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + startWithContinuations( + computation, + _emptyContinuation, + (dynamic error) { + throw error; + }, + _emptyContinuation, + cancellationToken, + ); +} + +void startImmediate( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + start(computation, cancellationToken); +} + +void startWithContinuations( + async_builder.Async computation, + Function continuation, + async_builder.Continuation exceptionContinuation, + async_builder.Continuation + cancellationContinuation, [ + types.Some? cancelToken, +]) { + final trampoline = async_builder.Trampoline(); + + async_builder.Continuation doneSuccess(Function cont) { + return (value) { + trampoline.completed = true; + + if (cont is Function()) { + cont(); + } else { + cont(value); + } + }; + } + + async_builder.Continuation done(async_builder.Continuation cont) { + return (value) { + trampoline.completed = true; + cont(value); + }; + } + + computation( + async_builder.IAsyncContext( + onSuccess: doneSuccess(continuation), + onError: done(exceptionContinuation), + onCancel: done( + cancellationContinuation, + ), + cancelToken: cancelToken?.value ?? defaultCancellationToken, + trampoline: trampoline, + ), + ); +} + +async_builder.Async sleep(Object delay) { + return async_builder.protectedCont((ctx) { + late final dart_async.Timer timer; + int? listenerId; + var completed = false; + + timer = dart_async.Timer(_cancellationDelay(delay), () { + if (completed) { + return; + } + + completed = true; + + final id = listenerId; + + if (id != null) { + ctx.cancelToken.removeListener(id); + } + + ctx.onSuccess(null); + }); + + listenerId = ctx.cancelToken.addListener(() { + if (completed) { + return; + } + + completed = true; + timer.cancel(); + ctx.onCancel(async_builder.OperationCanceledException()); + }); + }); +} + +async_builder.Async awaitFuture(dart_async.Future future) { + return async_builder.protectedCont((ctx) { + future.then( + (value) { + ctx.onSuccess(value); + }, + onError: (Object error, StackTrace stackTrace) { + if (error is async_builder.OperationCanceledException) { + ctx.onCancel(error); + } else { + ctx.onError(error); + } + }, + ); + }); +} + +dart_async.Future startAsFuture( + async_builder.Async computation, [ + types.Some? cancellationToken, +]) { + final completer = dart_async.Completer(); + + startWithContinuations( + computation, + (value) { + completer.complete(value); + }, + (dynamic error) { + completer.completeError(error); + }, + (error) { + completer.completeError(error); + }, + cancellationToken, + ); + + return completer.future; +} diff --git a/src/fable-library-dart/AsyncBuilder.dart b/src/fable-library-dart/AsyncBuilder.dart new file mode 100644 index 0000000000..824d260378 --- /dev/null +++ b/src/fable-library-dart/AsyncBuilder.dart @@ -0,0 +1,334 @@ +import 'dart:async' as dart_async; + +import 'Types.dart' as types; + +typedef Continuation = void Function(T value); + +class OperationCanceledException extends types.ExceptionBase { + OperationCanceledException([String? message]) + : super(message ?? 'The operation was canceled'); +} + +typedef Continuations = + types.Tuple3< + Continuation, + Continuation, + Continuation + >; + +class CancellationToken implements types.IDisposable { + int _id = 0; + bool _cancelled; + dart_async.Timer? _cancelTimer; + final Map _listeners = {}; + + CancellationToken([this._cancelled = false]); + + bool get isCancelled => _cancelled; + + void cancel() { + if (_cancelled) { + return; + } + + _cancelTimer?.cancel(); + _cancelTimer = null; + + _cancelled = true; + + final listeners = _listeners.values.toList(growable: false); + _listeners.clear(); + + for (final listener in listeners) { + listener(); + } + } + + void cancelAfter(Duration delay) { + if (_cancelled) { + return; + } + + _cancelTimer?.cancel(); + + _cancelTimer = dart_async.Timer(delay, () { + _cancelTimer = null; + cancel(); + }); + } + + int addListener(void Function() f) { + if (_cancelled) { + f(); + return -1; + } + + final id = _id++; + _listeners[id] = f; + return id; + } + + bool removeListener(int id) { + return _listeners.remove(id) != null; + } + + types.IDisposable register(dynamic f, [dynamic state]) { + final id = addListener(state == null ? () => f() : () => f(state)); + + return _CancellationRegistration(this, id); + } + + @override + void Dispose() { + // Disposing a CancellationTokenSource does not request cancellation. + } +} + +class _CancellationRegistration implements types.IDisposable { + final CancellationToken _token; + final int _id; + + _CancellationRegistration(this._token, this._id); + + @override + void Dispose() { + _token.removeListener(_id); + } +} + +class Trampoline { + static const int maxTrampolineCallCount = 2000; + + int _callCount = 0; + + bool completed = false; + + bool incrementAndCheck() { + return _callCount++ > maxTrampolineCallCount; + } + + void hijack(void Function() f) { + _callCount = 0; + dart_async.Timer.run(f); + } +} + +class IAsyncContext { + final Continuation onSuccess; + final Continuation onError; + final Continuation onCancel; + + final CancellationToken cancelToken; + final Trampoline trampoline; + + const IAsyncContext({ + required this.onSuccess, + required this.onError, + required this.onCancel, + required this.cancelToken, + required this.trampoline, + }); +} + +typedef Async = void Function(IAsyncContext context); + +// Binders arrive as zero- or one-argument functions with no inferred type. +dynamic invokeBinder(Function binder, T value) { + if (binder is Function()) { + return binder(); + } + + return binder(value); +} + +Async invokeBinderAsAsync(Function binder, T value) { + final dynamic bound = invokeBinder(binder, value); + + return (ctx) => bound(ctx); +} + +Async protectedCont(Async f) { + return (ctx) { + if (ctx.cancelToken.isCancelled) { + ctx.onCancel(OperationCanceledException()); + } else if (ctx.trampoline.incrementAndCheck()) { + ctx.trampoline.hijack(() { + try { + f(ctx); + } catch (error) { + if (ctx.trampoline.completed) { + rethrow; + } + + ctx.onError(error); + } + }); + } else { + try { + f(ctx); + } catch (error) { + if (ctx.trampoline.completed) { + rethrow; + } + + ctx.onError(error); + } + } + }; +} + +Async protectedBind(Async computation, Function binder) { + return protectedCont((ctx) { + computation( + IAsyncContext( + onSuccess: (value) { + final dynamic bound; + + try { + bound = invokeBinder(binder, value); + } catch (error) { + ctx.onError(error); + return; + } + + bound(ctx); + }, + onError: ctx.onError, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); +} + +Async protectedReturn(T value) { + return protectedCont((ctx) => ctx.onSuccess(value)); +} + +class AsyncBuilder { + Async Bind(Async computation, Function binder) { + return protectedBind(computation, binder); + } + + Async Combine(Async computation1, Async computation2) { + return Bind(computation1, () => computation2); + } + + Async Delay(Async Function() generator) { + return protectedCont((ctx) => generator()(ctx)); + } + + Async For(Iterable sequence, Function body) { + return Delay(() { + final iterator = sequence.iterator; + + Async loop() { + return Delay(() { + if (!iterator.moveNext()) { + return Zero(); + } + + final current = iterator.current; + + return Bind( + invokeBinderAsAsync(body, current), + loop, + ); + }); + } + + return loop(); + }); + } + + Async Return(T value) { + return protectedReturn(value); + } + + Async ReturnFrom(Async computation) { + return computation; + } + + Async TryFinally(Async computation, void Function() compensation) { + return protectedCont((ctx) { + void finish(void Function() continuation) { + try { + compensation(); + } catch (error) { + ctx.onError(error); + return; + } + + continuation(); + } + + computation( + IAsyncContext( + onSuccess: (value) { + finish(() => ctx.onSuccess(value)); + }, + onError: (error) { + finish(() => ctx.onError(error)); + }, + onCancel: (error) { + finish(() => ctx.onCancel(error)); + }, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + ), + ); + }); + } + + Async TryWith( + Async computation, + Async Function(dynamic error) catchHandler, + ) { + return protectedCont((ctx) { + computation( + IAsyncContext( + onSuccess: ctx.onSuccess, + onCancel: ctx.onCancel, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + onError: (error) { + late final Async handled; + + try { + handled = catchHandler(error); + } catch (handlerError) { + ctx.onError(handlerError); + return; + } + + handled(ctx); + }, + ), + ); + }); + } + + Async Using( + T resource, + Async Function(T value) binder, + ) { + return TryFinally(Delay(() => binder(resource)), resource.Dispose); + } + + Async While(bool Function() guard, Async computation) { + return Delay(() { + if (guard()) { + return Bind(computation, () => While(guard, computation)); + } + + return Return(null); + }); + } + + Async Zero() { + return protectedCont((ctx) => ctx.onSuccess(null)); + } +} + +final singleton = AsyncBuilder(); diff --git a/src/fable-library-dart/Fable.Library.Dart.fsproj b/src/fable-library-dart/Fable.Library.Dart.fsproj index 962c063607..382640cb7d 100644 --- a/src/fable-library-dart/Fable.Library.Dart.fsproj +++ b/src/fable-library-dart/Fable.Library.Dart.fsproj @@ -22,6 +22,8 @@ + + diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart index a6b380a18a..a177d68b7f 100644 --- a/tests/Dart/main.dart +++ b/tests/Dart/main.dart @@ -1,4 +1,5 @@ import './src/ArithmeticTests.dart' as arithmetic; +import './src/AsyncTests.dart' as async_; import './src/RandomTests.dart' as random; import './src/ArrayTests.dart' as array; import './src/ComparisonTests.dart' as comparison; @@ -32,6 +33,7 @@ import './src/UnionTests.dart' as union; void main() { arithmetic.tests(); + async_.tests(); random.tests(); array.tests(); comparison.tests(); diff --git a/tests/Dart/src/AsyncTests.fs b/tests/Dart/src/AsyncTests.fs new file mode 100644 index 0000000000..111dc43a0e --- /dev/null +++ b/tests/Dart/src/AsyncTests.fs @@ -0,0 +1,838 @@ +module Fable.Tests.Dart.Async + +open System +open Fable.Core.Dart +open Util + +type DisposableAction(f) = + interface IDisposable with + member _.Dispose() = f () + +type MyException(value) = + inherit Exception() + member _.Value: int = value + +let successWork: Async = Async.FromContinuations(fun (onSuccess, _, _) -> onSuccess "success") + +let errorWork: Async = Async.FromContinuations(fun (_, onError, _) -> onError (exn "error")) + +let cancelWork: Async = + Async.FromContinuations(fun (_, _, onCancel) -> System.OperationCanceledException("cancelled") |> onCancel) + +let asyncMap f a = + async { + let! a = a + return f a + } + +let sleepAndAssign token (res: bool ref) = + Async.StartImmediate( + async { + do! Async.Sleep 200 + res.Value <- true + }, + token + ) + +let tests () = + testCase "Simple async translates without exception" + <| fun () -> async { return () } |> Async.StartImmediate + + testCase "Async while binding works correctly" + <| fun () -> + let mutable result = 0 + + async { + while result < 10 do + result <- result + 1 + } + |> Async.StartImmediate + + equal 10 result + + testCase "Async for binding works correctly" + <| fun () -> + let inputs = [| 1; 2; 3 |] + let mutable result = 0 + + async { + for inp in inputs do + result <- result + inp + } + |> Async.StartImmediate + + equal 6 result + + testCase "Async exceptions are handled correctly" + <| fun () -> + let mutable result = 0 + + let f shouldThrow = + async { + try + if shouldThrow then + failwith "boom!" + else + result <- 12 + with _ -> + result <- 10 + } + |> Async.StartImmediate + + result + + f true + f false |> equal 22 + + testCase "Non captured exception in async is propagated when using Async.StartImmediate" + <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.StartImmediate) + + // Behaviour of Async.Start is the same as Async.StartImmediate in Dart + testCase "Non captured exception in async is propagated when using Async.Start" + <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.Start) + + testCase "Simple async is executed correctly" + <| fun () -> + let mutable result = false + let x = async { return 99 } + + async { + let! x = x + let y = 99 + result <- x = y + } + |> Async.StartImmediate + + equal true result + + testCase "async use statements should dispose of resources when they go out of scope" + <| fun () -> + let mutable isDisposed = false + let mutable step1ok = false + let mutable step2ok = false + + let resource = async { return new DisposableAction(fun () -> isDisposed <- true) } + + async { + use! _r = resource + step1ok <- not isDisposed + } + |> Async.StartImmediate + + step2ok <- isDisposed + + (step1ok && step2ok) |> equal true + + testCase "Try ... with ... expressions inside async expressions work the same" + <| fun () -> + let mutable result = "" + + let throw () : unit = raise (exn "Boo!") + + let append x = result <- result + x + + let innerAsync () = + async { + append "b" + + try + append "c" + throw () + append "1" + with _ -> + append "d" + + append "e" + } + + async { + append "a" + + try + do! innerAsync () + with _ -> + append "2" + + append "f" + } + |> Async.StartImmediate + + equal "abcdef" result + + testCaseAsync "Async cancellation works" + <| fun () -> + async { + let res1, res2, res3 = ref false, ref false, ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let tcs2 = new System.Threading.CancellationTokenSource() + let tcs3 = new System.Threading.CancellationTokenSource() + sleepAndAssign tcs1.Token res1 + sleepAndAssign tcs2.Token res2 + sleepAndAssign tcs3.Token res3 + tcs2.Cancel() + tcs3.CancelAfter(1000) + do! Async.Sleep 500 + equal false res1.Value + equal false res2.Value + equal true res3.Value + } + + testCaseAsync "CancellationTokenSource.Register works" + <| fun () -> + async { + let mutable x = 0 + let res1 = ref false + let tcs1 = new System.Threading.CancellationTokenSource(50) + let _foo = tcs1.Token.Register(fun () -> x <- x + 1) + sleepAndAssign tcs1.Token res1 + do! Async.Sleep 500 + equal false res1.Value + equal 1 x + } + + testCaseAsync "CancellationToken can be disposed" + <| fun () -> // See #2879 + async { + let res1 = ref false + + do + use tcs1 = new System.Threading.CancellationTokenSource() + sleepAndAssign tcs1.Token res1 + + do! Async.Sleep 300 + // Disposing the CancellationToken doesn't cancel the operation + equal true res1.Value + } + + testCase "Async.StartWithContinuations works" + <| fun () -> + let mutable res1 = "" + let mutable res2 = "" + let mutable res3 = "" + Async.StartWithContinuations(successWork, (fun x -> res1 <- x), ignore, ignore) + Async.StartWithContinuations(errorWork, ignore, (fun x -> res2 <- x.Message), ignore) + Async.StartWithContinuations(cancelWork, ignore, ignore, (fun x -> res3 <- x.Message)) + equal "success" res1 + equal "error" res2 + equal "cancelled" res3 + + testCase "Async.Catch works" + <| fun () -> + let mutable res1 = "" + let mutable res2 = "" + + let assign = + function + | Choice1Of2 msg -> msg + | Choice2Of2(ex: Exception) -> "ERROR: " + ex.Message + + async { + let! x1 = successWork |> Async.Catch + res1 <- assign x1 + let! x2 = errorWork |> Async.Catch + res2 <- assign x2 + } + |> Async.StartImmediate + + equal "success" res1 + equal "ERROR: error" res2 + + testCase "Async.Ignore works" + <| fun () -> + let mutable res = false + + async { + do! successWork |> Async.Ignore + res <- true + } + |> Async.StartImmediate + + equal true res + + testCaseAsync "Async.Parallel works" + <| fun () -> + async { + let makeWork i = + async { + do! Async.Sleep 200 + return i + } + + let mutable res: int[] = [||] + let works = [ makeWork 1; makeWork 2; makeWork 3 ] + + async { + let! x = Async.Parallel works + res <- x + } + |> Async.StartImmediate + + do! Async.Sleep 500 + res |> Array.sum |> equal 6 + } + + testCaseAsync "Async.Parallel is lazy" + <| fun () -> + async { + let mutable x = 0 + + let add i = x <- x + i + + let a = Async.Parallel [ async { add 1 }; async { add 2 } ] + + do! Async.Sleep 100 + + equal 0 x + + let! _ = a + + equal 3 x + } + + testCaseAsync "Async.Sequential works" + <| fun () -> + async { + let mutable aggregate = 0 + + let makeWork i = + async { + // check that the individual work items run sequentially and not interleaved + aggregate <- aggregate + i + let copyOfI = aggregate + do! Async.Sleep 100 + equal copyOfI aggregate + do! Async.Sleep 100 + equal copyOfI aggregate + return i + } + + let works = [ for i in 1..5 -> makeWork i ] + let! result = Async.Sequential works + result |> equal [| 1..5 |] + result |> Array.sum |> equal aggregate + } + + testCaseAsync "Async.Sequential is lazy" + <| fun () -> + async { + let mutable x = 0 + + let a = Async.Sequential [ async { x <- x + 1 }; async { x <- x + 2 } ] + + do! Async.Sleep 100 + + equal 0 x + + let! _ = a + + equal 3 x + } + + testCaseAsync "Interaction between Async and Future works" + <| fun () -> + async { + let mutable res = false + + async { res <- true } + |> Async.StartAsFuture + |> Async.AwaitFuture + |> Async.StartImmediate + + equal true res + } + + testCaseAsync "Futures can be cancelled" + <| fun () -> + async { + let mutable res = 0 + let tcs = new System.Threading.CancellationTokenSource(50) + + let work = + let work = + async { + do! Async.Sleep 75 + res <- -1 + } + + Async.StartAsFuture(work, tcs.Token) |> Async.AwaitFuture + + Async.StartWithContinuations(work, ignore, ignore, (fun _ -> res <- 1)) + do! Async.Sleep 100 + equal 1 res + } + + testCaseAsync "Async.AwaitFuture propagates Future errors" + <| fun () -> + async { + let future = + async { + failwith "boom" + return 0 + } + |> Async.StartAsFuture + + let mutable message = "" + + try + let! _ = Async.AwaitFuture future + () + with error -> + message <- error.Message + + equal "boom" message + } + + testCase "Async try .. with returns correctly from 'with' branch" + <| fun () -> + let work = + async { + try + failwith "testing" + return -1 + with e -> + return 42 + } + + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 42 result + + testCaseAsync "Deep recursion with async doesn't cause stack overflow" + <| fun () -> + async { + let mutable result = false + + let rec trampolineTest i = + async { + if i > 100000 then + result <- true + else + return! trampolineTest (i + 1) + } + + do! trampolineTest 0 + equal true result + } + + testCaseAsync "Nested failure propagates in async expressions" + <| fun () -> + async { + let mutable data = "" + + let f1 x = + async { + try + failwith "1" + return x + with e -> + return! failwith ("2 " + e.Message) + } + + let f2 x = + async { + try + return! f1 x + with e -> + return! failwith ("3 " + e.Message) + } + + let f () = + async { + try + let! _y = f2 4 + return () + with e -> + data <- e.Message + } + |> Async.StartImmediate + + f () + do! Async.Sleep 100 + equal "3 2 1" data + } + + testCaseAsync "Try .. finally expressions inside async expressions work" + <| fun () -> + async { + let mutable data = "" + + async { + try + data <- data + "1 " + finally + data <- data + "2 " + } + |> Async.StartImmediate + + async { + try + try + failwith "boom!" + finally + data <- data + "3" + with _ -> + () + } + |> Async.StartImmediate + + do! Async.Sleep 100 + equal "1 2 3" data + } + + testCaseAsync "Final statement inside async expressions can throw" + <| fun () -> + async { + let mutable data = "" + + let f () = + async { + try + data <- data + "1 " + finally + failwith "boom!" + } + + async { + try + do! f () + return () + with e -> + data <- data + e.Message + } + |> Async.StartImmediate + + do! Async.Sleep 100 + equal "1 boom!" data + } + + testCaseAsync "Async.Bind propagates exceptions" + <| fun () -> // See #724 + async { + let task1 name = + async { + if name = "fail" then + failwith "Invalid access credentials" + + return "Ok" + } + + let task2 name = + async { + do! Async.Sleep 100 //difference between task1 and task2 + + if name = "fail" then + failwith "Invalid access credentials" + + return "Ok" + } + + let doWork _name task = + let catch comp = + async { + let! res = Async.Catch comp + + return + match res with + | Choice1Of2 str -> str + | Choice2Of2 ex -> ex.Message + } + + async { + let! a = task "work" |> catch + let! b = task "fail" |> catch + return a, b + } + + let! res1 = doWork "task1" task1 + let! res2 = doWork "task2" task2 + equal ("Ok", "Invalid access credentials") res1 + equal ("Ok", "Invalid access credentials") res2 + } + + testCaseAsync "Async.StartChild works" + <| fun () -> + async { + let mutable x = "" + + let taskA = + async { + do! Async.Sleep 500 + x <- x + "D" + return "E" + } + + let taskB = + async { + do! Async.Sleep 100 + x <- x + "C" + return "F" + } + + let! result1Async = taskA |> Async.StartChild // start first request but do not wait + let! result2Async = taskB |> Async.StartChild // start second request in parallel + x <- x + "AB" + let! result1 = result1Async + let! result2 = result2Async + x <- x + result1 + result2 + equal "ABCDEF" x + } + + testCaseAsync "Async.StartChild applies timeout" + <| fun () -> + async { + let mutable x = "" + + let task = + async { + x <- x + "A" + do! Async.Sleep 1_000 + x <- x + "X" // Never hit + } + + try + let! childTask = Async.StartChild(task, 200) + + do! childTask + with :? TimeoutException -> + x <- x + "B" + + x <- x + "C" + + equal "ABC" x + } + + testCaseAsync "Async.StartChild with timeout completes when computation finishes before timeout" + <| fun () -> // See #4481 + async { + let fast = async { do! Async.Sleep 10 } + + try + let! child = Async.StartChild(fast, 1_000) + do! child + equal true true // should reach here + with :? TimeoutException -> + failwith "should not time out" + } + + testCaseAsync "Unit arguments are erased" + <| fun () -> // See #1832 + let mutable token = 0 + + async { + let! res = async.Return 5 |> asyncMap (fun x -> token <- x) + equal 5 token + res + } + + testCaseAsync "Can use custom exceptions in async workflows #2396" + <| fun () -> + let workflow () : Async = async { return MyException(7) |> raise } + + let parentWorkflow () = + async { + try + do! workflow () + return 100 + with :? MyException as ex -> + return ex.Value + } + + async { + let! res = parentWorkflow () + equal 7 res + } + + testCase "Async try .. with does not run 'with' branch when body succeeds" + <| fun () -> + let work = + async { + try + return 1 + with _ -> + return 99 + } + + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 1 result + + testCase "Exception thrown by the continuation is not redirected to the 'with' branch" + <| fun () -> + let work = + async { + try + return 1 + with _ -> + return 99 + } + + let mutable calls = 0 + + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun _ -> + calls <- calls + 1 + failwith "boom from continuation"), + ignore, + ignore + ) + ) + + equal 1 calls + + testCaseAsync "Async.StartChild cancels the child when the parent's token is cancelled" + <| fun () -> + async { + let mutable finallyRan = false + let mutable completed = false + let cts = new System.Threading.CancellationTokenSource() + + let child = + async { + try + do! Async.Sleep 500 + completed <- true + finally + finallyRan <- true + } + + let parent = + async { + let! childResult = Async.StartChild child + do! childResult + } + + Async.StartImmediate(parent, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal true finallyRan + equal false completed + } + + testCaseAsync "Async.Parallel children are cancelled when the parent's token is cancelled" + <| fun () -> + async { + let mutable completed = 0 + let cts = new System.Threading.CancellationTokenSource() + + let mkChild () = + async { + do! Async.Sleep 500 + completed <- completed + 1 + } + + let work = + async { + let! _ = Async.Parallel [ mkChild (); mkChild (); mkChild () ] + return () + } + + Async.StartImmediate(work, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal 0 completed + } + + testCaseAsync "Async.Sequential children are cancelled when the parent's token is cancelled" + <| fun () -> + async { + let mutable completed = 0 + let cts = new System.Threading.CancellationTokenSource() + + let mkChild () = + async { + do! Async.Sleep 500 + completed <- completed + 1 + } + + let work = + async { + let! _ = Async.Sequential [ mkChild (); mkChild (); mkChild () ] + return () + } + + Async.StartImmediate(work, cts.Token) + do! Async.Sleep 200 + cts.Cancel() + do! Async.Sleep 600 + equal 0 completed + } + + testCase "CancellationTokenSource.Cancel invokes registrations only once" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable calls = 0 + + cts.Token.Register(fun () -> calls <- calls + 1) |> ignore + + cts.Cancel() + cts.Cancel() + + equal 1 calls + + testCase "CancellationToken registration can be disposed" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable called = false + + let registration = cts.Token.Register(fun () -> called <- true) + + registration.Dispose() + + cts.Cancel() + + equal false called + + testCase "CancellationToken registration after cancellation runs immediately" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + let mutable called = false + + cts.Cancel() + + cts.Token.Register(fun () -> called <- true) |> ignore + + equal true called + + testCase "ThrowIfCancellationRequested throws after cancellation" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + + // Does not throw before cancellation. + cts.Token.ThrowIfCancellationRequested() + + cts.Cancel() + + throwsAnyError (fun () -> cts.Token.ThrowIfCancellationRequested()) + + testCase "Async.CancellationToken returns current cancellation token" + <| fun () -> + let cts = new System.Threading.CancellationTokenSource() + + let work = + async { + let! token = Async.CancellationToken + token = cts.Token |> equal true + } + + Async.StartWithContinuations(work, ignore, raise, raise, cts.Token) + + testCaseAsync "CancellationTokenSource.CancelAfter can be reset" + <| fun () -> + async { + let cts = new System.Threading.CancellationTokenSource() + + cts.CancelAfter 300 + do! Async.Sleep 30 + + cts.CancelAfter 500 + do! Async.Sleep 350 + + equal false cts.IsCancellationRequested + + do! Async.Sleep 200 + + equal true cts.IsCancellationRequested + } diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index 2fe141bc5a..55750b54b8 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -9,6 +9,7 @@ + diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index 9091e038e1..4e0754c735 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -1,6 +1,7 @@ module Fable.Tests.Dart.Util open Fable.Core +open Fable.Core.Dart type Assertion = interface end @@ -8,12 +9,14 @@ type Assertion = [] type Test = static member test(msg: string, f: unit -> unit): unit = nativeOnly + static member test(msg: string, f: unit -> Future): unit = nativeOnly static member expect(actual: obj, assertion: Assertion): unit = nativeOnly static member equals(value: obj): Assertion = nativeOnly static member isNot(assertion: Assertion): Assertion = nativeOnly static member throwsException: Assertion = nativeOnly let testCase (msg: string) (f: unit -> unit) = Test.test(msg, f) +let testCaseAsync (msg: string) (f: unit -> Async) = Test.test(msg, fun () -> f () |> Async.StartAsFuture) let testList (msg: string) (tests: unit list) = () let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected)) let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) From c9f818a42d9dcf06e080607e4f3d699eedd46660 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:57:40 +0200 Subject: [PATCH 079/322] chore: Bump the all-nuget group with 6 updates (#4976) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dag Brattli --- .config/dotnet-tools.json | 4 +-- src/Directory.Build.props | 4 +-- src/Fable.Analyzers/Fable.Analyzers.fsproj | 2 +- src/Fable.Compiler/BrowserInlineExprs.fs | 4 +-- src/Fable.Transforms/Babel/Fable2Babel.fs | 5 ++-- src/Fable.Transforms/Beam/Fable2Beam.Util.fs | 2 +- src/Fable.Transforms/Dart/Replacements.fs | 2 +- src/Fable.Transforms/FSharp2Fable.Util.fs | 4 +-- .../Python/Fable2Python.Transforms.fs | 5 +++- .../Rust/AST/Rust.AST.Helpers.fs | 4 +-- .../Rust/AST/Rust.AST.Impl.fs | 2 +- .../Rust/AST/Rust.AST.Parser.fs | 2 +- .../Rust/AST/Rust.AST.Pretty.fs | 14 +++++----- .../Rust/AST/Rust.AST.State.fs | 28 +++++++++---------- src/Fable.Transforms/Rust/AST/Tests/Tests.fs | 4 +-- src/Fable.Transforms/Rust/Fable2Rust.fs | 6 ++-- src/fable-compiler-js/src/app.fs | 2 +- src/fable-library-rust/src/Quotation.fs | 24 ++++++++-------- .../src/System.Collections.Generic.fs | 4 +-- .../System.Collections.Generic.fs | 4 +-- src/fable-standalone/src/Worker/Shared.fs | 4 +-- src/fable-standalone/src/Worker/Worker.fs | 10 ++++--- .../test/bench-compiler/app.fs | 2 +- tests/Python/Fable.Tests.Python.fsproj | 2 +- tests/Rust/Fable.Tests.Rust.fsproj | 2 +- 25 files changed, 76 insertions(+), 70 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dd7d9b9e20..bfda1de7f4 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "7.0.6", + "version": "8.0.0", "commands": [ "fantomas" ], @@ -17,7 +17,7 @@ "rollForward": false }, "fsharp-analyzers": { - "version": "0.38.0", + "version": "0.39.2", "commands": [ "fsharp-analyzers" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d9eff242ce..d5525ec703 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -28,11 +28,11 @@ all build - + all analyzers - + all analyzers diff --git a/src/Fable.Analyzers/Fable.Analyzers.fsproj b/src/Fable.Analyzers/Fable.Analyzers.fsproj index 81249e8a5a..26837db080 100644 --- a/src/Fable.Analyzers/Fable.Analyzers.fsproj +++ b/src/Fable.Analyzers/Fable.Analyzers.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/Fable.Compiler/BrowserInlineExprs.fs b/src/Fable.Compiler/BrowserInlineExprs.fs index e8aaeb4c2f..ab4c98a449 100644 --- a/src/Fable.Compiler/BrowserInlineExprs.fs +++ b/src/Fable.Compiler/BrowserInlineExprs.fs @@ -25,13 +25,13 @@ let private extra = (Decode.uint64 |> Decode.map unativeint) let private decoder = - lazy (Decode.Auto.generateDecoder<(string * Fable.InlineExpr) array> (extra = extra)) + lazy (Decode.Auto.generateDecoder<(string * Fable.InlineExpr) array>(extra = extra)) let fromString (json: string) : Result<(string * Fable.InlineExpr) array, string> = Decode.fromString decoder.Value json #if !FABLE_COMPILER let private encoder = - lazy (Encode.Auto.generateEncoder<(string * Fable.InlineExpr) array> (extra = extra)) + lazy (Encode.Auto.generateEncoder<(string * Fable.InlineExpr) array>(extra = extra)) let toString (chunk: (string * Fable.InlineExpr) array) : string = chunk |> encoder.Value |> Encode.toString 0 diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 6dc75f53db..1b62340a4f 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -866,7 +866,7 @@ module Annotation = let makeStringEnumTypeAnnotation (ent: Fable.Entity) (attArgs: obj list) = let rule = match List.tryHead attArgs with - | Some(:? int as rule) -> enum (rule) + | Some(:? int as rule) -> enum(rule) | _ -> Core.CaseRules.LowerFirst ent.UnionCases @@ -896,7 +896,7 @@ module Annotation = let makeTypeScriptTaggedUnionTypeAnnotation com ctx genArgs (ent: Fable.Entity) (attArgs: obj list) = let tag, rule = match attArgs with - | (:? string as tag) :: (:? int as rule) :: _ -> tag, enum (rule) + | (:? string as tag) :: (:? int as rule) :: _ -> tag, enum(rule) | (:? string as tag) :: _ -> tag, Core.CaseRules.LowerFirst | _ -> "kind", Core.CaseRules.LowerFirst @@ -2213,6 +2213,7 @@ but thanks to the optimisation done below we get | _ -> ValueNone // Make it easy to check if we are calling the expected function + and [] (|CalledExpression|_|) (callerText: string) value = match value with | CallExpression(IdentifierIs callerText, UnrollerFromArray exprs, _, _) -> ValueSome exprs diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs index 727237303d..501518a8e4 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs @@ -63,7 +63,7 @@ let private wrappedLiteral (bits: int, signed: bool) (value: int64) = if not signed && truncated < 0L then // 64-bit unsigned above Int64.MaxValue: does not fit an Erlang int64 literal - Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string (uint64 truncated))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string(uint64 truncated))) else Beam.ErlExpr.Literal(Beam.ErlLiteral.Integer truncated) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 94702f7a64..7cbf020b62 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1639,7 +1639,7 @@ let formattableString |} ) - printJsTaggedTemplate str holes (fun i -> "$" + string (i + offset)) + printJsTaggedTemplate str holes (fun i -> "$" + string(i + offset)) emitExpr r t args (callMacro + jsTaggedTemplate) |> Some | "get_Format", Some x, _ -> Helper.LibCall(com, "String", "getFormat", t, [ x ], ?loc = r) |> Some diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index d9d9dd3317..61eb69055e 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1075,7 +1075,7 @@ module Helpers = let getCaseRule (att: FSharpAttribute) = match Seq.tryHead att.ConstructorArguments with - | Some(_, (:? int as rule)) -> enum (rule) + | Some(_, (:? int as rule)) -> enum(rule) | _ -> CaseRules.LowerFirst unionCase.Attributes @@ -1103,7 +1103,7 @@ module Helpers = | Some(_, (:? string as name)), None -> Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, CaseRules.LowerFirst)) | Some(_, (:? string as name)), Some(_, (:? int as rule)) -> - Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, enum (rule))) + Some(TypeScriptTaggedUnion(tdef, typ.GenericArguments, name, enum(rule))) | _ -> failwith "Invalid TypeScriptTaggedUnion attribute" | _ -> None ) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 1c71ea9c0a..5bde8afa1f 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1706,7 +1706,10 @@ let transformGet (com: IPythonCompiler) ctx range typ (fableExpr: Fable.Expr) ki let caseRef = getUnionCaseRef com ctx i.Entity ent uci let narrow = libValue com ctx "union" "narrow" let narrowedExpr = Expression.call (narrow, [ caseRef; baseExpr ]) - let! finalExpr = getExpr com ctx range narrowedExpr (Expression.stringConstant fieldName) + + let! finalExpr = + getExpr com ctx range narrowedExpr (Expression.stringConstant fieldName) + return finalExpr } diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs index 7b6a4d5f56..f2eecc6918 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs @@ -202,7 +202,7 @@ module Literals = let mkCharLit (value: char) : Lit = { - token = mkCharTokenLit ((string value).escape_debug ()) + token = mkCharTokenLit ((string value).escape_debug()) kind = LitKind.Char(value) span = DUMMY_SP } @@ -890,7 +890,7 @@ module Exprs = value.StartsWith("\"", System.StringComparison.Ordinal) && value.EndsWith("\"", System.StringComparison.Ordinal) then - "\"" + value[1 .. (value.Length - 2)].escape_debug () + "\"" + "\"" + value[1 .. (value.Length - 2)].escape_debug() + "\"" else value diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs index 312811ee86..c0222bd999 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Impl.fs @@ -374,7 +374,7 @@ type Expr with type MacCall with member self.span() : Span = - self.path.span.to_ (self.args.span().unwrap_or (self.path.span)) + self.path.span.to_ (self.args.span().unwrap_or(self.path.span)) type MacArgs with diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs index 293a73d302..8b48ed95d2 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Parser.fs @@ -368,7 +368,7 @@ type ExprPrecedence with | ExprPrecedence.Range -> PREC_RANGE // Binop-like expr kinds, handled by `AssocOp`. - | ExprPrecedence.Binary(op) -> AssocOp.from_ast_binop(op).precedence () + | ExprPrecedence.Binary(op) -> AssocOp.from_ast_binop(op).precedence() | ExprPrecedence.Cast -> AssocOp.As.precedence () | ExprPrecedence.Type -> AssocOp.Colon.precedence () diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs index a07e989131..a63a99f7b6 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Pretty.fs @@ -421,11 +421,11 @@ type Printer with self.buf[self.right] <- entry self.scan_stack.push_front (self.right) - member self.scan_pop() : usize = self.scan_stack.pop_front().unwrap () + member self.scan_pop() : usize = self.scan_stack.pop_front().unwrap() - member self.scan_top() : usize = self.scan_stack.front().unwrap () + member self.scan_top() : usize = self.scan_stack.front().unwrap() - member self.scan_pop_bottom() : usize = self.scan_stack.pop_back().unwrap () + member self.scan_pop_bottom() : usize = self.scan_stack.pop_back().unwrap() member self.advance_right() = self.right <- self.right + 1 @@ -501,7 +501,7 @@ type Printer with member self.get_top() : PrintStackElem = self.print_stack .last() - .unwrap_or ( + .unwrap_or( { offset = 0 pbreak = PrintStackBreak.Broken(Breaks.Inconsistent) @@ -531,7 +531,7 @@ type Printer with member self.print_end() = debug ("print End -> pop End") - self.print_stack.pop().unwrap () |> ignore + self.print_stack.pop().unwrap() |> ignore member self.print_break(b: BreakToken, l: isize) = let top = self.get_top () @@ -573,7 +573,7 @@ type Printer with // self.out.reserve(self.pending_indentation) // self.out.extend(std.iter.repeat(' ').take(self.pending_indentation)) - self.out.push_str (" ".repeat (self.pending_indentation)) + self.out.push_str (" ".repeat(self.pending_indentation)) self.pending_indentation <- 0 self.out.push_str (s) @@ -637,7 +637,7 @@ type Printer with member self.hardbreak() = self.spaces (_SIZE_INFINITY) member self.is_beginning_of_line() : bool = - self.last_token().is_eof () || self.last_token().is_hardbreak_tok () + self.last_token().is_eof() || self.last_token().is_hardbreak_tok() static member hardbreak_tok_offset(off: isize) : Token = Token.Break( diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs index f4c59f6dae..ded92c2033 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.State.fs @@ -221,11 +221,11 @@ let literal_to_string (lit: token.Lit) : string = | token.LitKind.Char -> format ("'{0}'", symbol) | token.LitKind.Str -> format ("\"{0}\"", symbol) | token.LitKind.StrRaw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("r{0}\"{1}\"{2}", delim, symbol, delim) | token.LitKind.ByteStr -> format ("b\"{0}\"", symbol) | token.LitKind.ByteStrRaw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("br{0}\"{1}\"{2}", delim, symbol, delim) | token.LitKind.Integer | token.LitKind.Float @@ -238,7 +238,7 @@ let literal_to_string (lit: token.Lit) : string = // out.push_str(suffix.as_str()) let visibility_qualified (vis: ast.Visibility, s: string) : string = - let vis_str = State.new_().to_string (fun (s) -> s.print_visibility (vis)) + let vis_str = State.new_().to_string(fun (s) -> s.print_visibility (vis)) format ("{0}{1}", vis_str, s) // interface std.ops.Deref with // for State @@ -440,7 +440,7 @@ type State with | Some(cmnts) -> cmnts.current <- cmnts.current + 1 member self.next_comment() : Option = - self.comments().and_then (fun (c) -> c.next ()) + self.comments().and_then(fun (c) -> c.next ()) member self.print_literal(lit: ast.Lit) = self.maybe_print_comment (lit.span.lo ()) @@ -451,7 +451,7 @@ type State with match style with | ast.StrStyle.Cooked -> format ("\"{0}\"", st.escape_debug ()) | ast.StrStyle.Raw(n) -> - let delim = "#".repeat (n |> usize) + let delim = "#".repeat(n |> usize) format ("r{0}\"{1}\"{2}", delim, st, delim) self.s.word (st) @@ -701,7 +701,7 @@ type State with member self.break_offset_if_not_bol(n: usize, off: isize) = if not (self.s.is_beginning_of_line ()) then self.s.break_offset (n, off) - elif off <> 0 && self.s.last_token().is_hardbreak_tok () then + elif off <> 0 && self.s.last_token().is_hardbreak_tok() then // We do something pretty sketchy here: tuck the nonzero // offset-adjustment we were going to deposit along with the // break into the previous hardbreak. @@ -717,7 +717,7 @@ type State with | token.Nonterminal.NtBlock(e) -> self.block_to_string (e) | token.Nonterminal.NtStmt(e) -> self.stmt_to_string (e) | token.Nonterminal.NtPat(e) -> self.pat_to_string (e) - | token.Nonterminal.NtIdent(e, is_raw) -> IdentPrinter.for_ast_ident(e, is_raw).to_string () + | token.Nonterminal.NtIdent(e, is_raw) -> IdentPrinter.for_ast_ident(e, is_raw).to_string() | token.Nonterminal.NtLifetime(e) -> e.to_string () | token.Nonterminal.NtLiteral(e) -> self.expr_to_string (e) | token.Nonterminal.NtTT(tree) -> self.tt_to_string (tree) @@ -774,7 +774,7 @@ type State with | token.TokenKind.Literal(lit) -> literal_to_string (lit) // Name components - | token.TokenKind.Ident(s, is_raw) -> IdentPrinter.new_(s, is_raw, convert_dollar_crate).to_string () + | token.TokenKind.Ident(s, is_raw) -> IdentPrinter.new_(s, is_raw, convert_dollar_crate).to_string() | token.TokenKind.Lifetime(s) -> s.to_string () // Other @@ -858,7 +858,7 @@ type State with member self.comments() : Option = self.comments_ member self.print_ident(ident: Ident) = - self.s.word (IdentPrinter.for_ast_ident(ident, ident.is_raw_guess ()).to_string ()) + self.s.word (IdentPrinter.for_ast_ident(ident, ident.is_raw_guess ()).to_string()) self.ann.post (self, AnnNode.Ident(ident)) @@ -912,14 +912,14 @@ type State with let mutable i = 0 for elt in elts do - self.maybe_print_comment (get_span(elt).hi ()) + self.maybe_print_comment (get_span(elt).hi()) op (self, elt) i <- i + 1 if i < len then self.s.word (",") - self.maybe_print_trailing_comment (get_span (elt), Some(get_span(elts[i]).hi ())) + self.maybe_print_trailing_comment (get_span (elt), Some(get_span(elts[i]).hi())) self.s.space_if_not_bol () @@ -1593,7 +1593,7 @@ type State with self.print_expr_cond_paren ( scrutinee, self.cond_needs_par (scrutinee) - || parser.needs_par_as_let_scrutinee (scrutinee.precedence().order ()) + || parser.needs_par_as_let_scrutinee (scrutinee.precedence().order()) ) member self.print_else(els: Option) = @@ -1645,7 +1645,7 @@ type State with self.s.pclose () member self.print_expr_maybe_paren(expr: ast.Expr, prec: i8) = - self.print_expr_cond_paren (expr, expr.precedence().order () < prec) + self.print_expr_cond_paren (expr, expr.precedence().order() < prec) /// Prints an expr using syntax that's acceptable in a condition position, such as the `cond` in /// `if cond then ... }`. @@ -2766,7 +2766,7 @@ type State with member self.print_remaining_comments() = // If there aren't any remaining comments, then we need to manually // make sure there is a line break at the end_. - if self.next_comment().is_none () then + if self.next_comment().is_none() then self.s.hardbreak () let mutable cmnt = self.next_comment () diff --git a/src/Fable.Transforms/Rust/AST/Tests/Tests.fs b/src/Fable.Transforms/Rust/AST/Tests/Tests.fs index d80fc81537..0a3eaa9985 100644 --- a/src/Fable.Transforms/Rust/AST/Tests/Tests.fs +++ b/src/Fable.Transforms/Rust/AST/Tests/Tests.fs @@ -17,7 +17,7 @@ module Helpers = let fun_to_string (decl: FnDecl, header: FnHeader, name: Ident, generics: Generics) : string = State .new_() - .to_string (fun (s) -> + .to_string(fun (s) -> s.head ("") s.print_fn (decl, header, Some(name), generics) s.s.end_ () // Close the head box. @@ -25,7 +25,7 @@ module Helpers = ) let variant_to_string (var: Variant) : string = - State.new_().to_string (fun (s) -> s.print_variant (var)) + State.new_().to_string(fun (s) -> s.print_variant (var)) [] let test_fun_to_string () = diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 1aab73ba9f..9f6c184b7b 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -899,7 +899,7 @@ module TypeInfo = | [ Fable.Unit ] -> [] | _ -> argTypes - let argCount = string (List.length argTypes) + let argCount = string(List.length argTypes) let genArgs = argTypes @ [ returnType ] transformImportType com ctx genArgs "Native" ("Func" + argCount) @@ -1047,7 +1047,7 @@ module TypeInfo = transformGenericType com ctx genArgs (rawIdent "Result") let transformChoiceType com ctx genArgs : Rust.Ty = - let argCount = string (List.length genArgs) + let argCount = string(List.length genArgs) transformImportType com ctx genArgs "Choice" ("Choice`" + argCount) let transformRefCellType com ctx genArg : Rust.Ty = @@ -1802,7 +1802,7 @@ module Util = | [ Fable.Unit ] -> [] | _ -> argTypes - let argCount = string (List.length argTypes) + let argCount = string(List.length argTypes) let funcWrap = getLibraryImportName com ctx "Native" ("Func" + argCount) let expr = transformIdent com ctx None ident diff --git a/src/fable-compiler-js/src/app.fs b/src/fable-compiler-js/src/app.fs index e9d64032e2..3b077acd13 100644 --- a/src/fable-compiler-js/src/app.fs +++ b/src/fable-compiler-js/src/app.fs @@ -179,7 +179,7 @@ type SourceWriter(sourcePath, targetPath, projDir, options: CmdLineOptions, file member _.Dispose() = () - member _.SourceMap = mapGenerator.Force().toJSON () + member _.SourceMap = mapGenerator.Force().toJSON() member _.Result = sb.ToString() let printErrors showWarnings (errors: Fable.Standalone.Error[]) = diff --git a/src/fable-library-rust/src/Quotation.fs b/src/fable-library-rust/src/Quotation.fs index feae382ce1..9cdebc2d7e 100644 --- a/src/fable-library-rust/src/Quotation.fs +++ b/src/fable-library-rust/src/Quotation.fs @@ -398,8 +398,8 @@ let private applyOperator (method: string) (args: obj list) : obj = // invalid at runtime. Matching only on the method string avoids that. // if/elif (not match): Fable-Rust would emit string literals as match patterns, // which isn't valid Rust; an if-chain compiles to string == comparisons. - let i (n: int) : int = unbox (List.item n args) - let b (n: int) : bool = unbox (List.item n args) + let i (n: int) : int = unbox(List.item n args) + let b (n: int) : bool = unbox(List.item n args) if method = "op_Addition" then box (i 0 + i 1) @@ -453,13 +453,13 @@ let evaluate (e: FSharpExpr) : obj = let capturedEnv = env.Value box (fun (arg: obj) -> eval (ref (Map.add v.Name arg capturedEnv)) body) | ExprApplication(f, a) -> - let func = unbox obj> (eval env f) + let func = unbox obj>(eval env f) func (eval env a) | ExprLet(v, value, body) -> let value = eval env value eval (ref (Map.add v.Name value env.Value)) body | ExprIfThenElse(g, t, el) -> - if unbox (eval env g) then + if unbox(eval env g) then eval env t else eval env el @@ -472,7 +472,7 @@ let evaluate (e: FSharpExpr) : obj = match method with | "get_IsSome" | "get_IsNone" -> - let values = unbox (eval env instance) + let values = unbox(eval env instance) let isSome = values.Length > 0 && unbox values.[0] = 1 box ( @@ -483,7 +483,7 @@ let evaluate (e: FSharpExpr) : obj = ) | "get_IsCons" | "get_IsEmpty" -> - let values = unbox (eval env instance) + let values = unbox(eval env instance) let isCons = values.Length > 0 box ( @@ -503,7 +503,7 @@ let evaluate (e: FSharpExpr) : obj = // operator wants an owned i32; `index + 0` forces an owned i32 rvalue (the // arithmetic dereferences the borrow) so the indexing type-checks. let i = index + 0 - evalArrayField (unbox (eval env inner)) i + evalArrayField (unbox(eval env inner)) i | ExprNewUnion(_, tag, _, fields) -> let values = ResizeArray() values.Add(box tag) @@ -524,18 +524,18 @@ let evaluate (e: FSharpExpr) : obj = let result = ResizeArray() result.Add(eval env head) - for value in unbox (eval env tail) do + for value in unbox(eval env tail) do result.Add(value) box (result.ToArray()) | ExprUnionTag inner -> - let values = unbox (eval env inner) + let values = unbox(eval env inner) evalArrayField values 0 | ExprUnionField(inner, fieldIndex) -> - let values = unbox (eval env inner) + let values = unbox(eval env inner) evalArrayField values (fieldIndex + 1) | ExprFieldGet(inner, fieldName) -> - let values = unbox (eval env inner) + let values = unbox(eval env inner) if fieldName = "Head" then evalArrayField values 0 @@ -557,7 +557,7 @@ let evaluate (e: FSharpExpr) : obj = | Some value -> value | None -> failwithf "Quotation field not found: %s" fieldName | ExprFieldSet(target, fieldName, value) -> - let target = unbox (eval env target) + let target = unbox(eval env target) let value = eval env value let rec findFieldIndex index = diff --git a/src/fable-library-rust/src/System.Collections.Generic.fs b/src/fable-library-rust/src/System.Collections.Generic.fs index ac2461e630..e4e5f79f17 100644 --- a/src/fable-library-rust/src/System.Collections.Generic.fs +++ b/src/fable-library-rust/src/System.Collections.Generic.fs @@ -48,7 +48,7 @@ type Stack<'T when 'T: equality> private (initialContents: 'T[], initialCount) = new(initialCapacity: int) = - let arr = Array.zeroCreate<'T> (initialCapacity) + let arr = Array.zeroCreate<'T>(initialCapacity) Stack<'T>(arr, 0) new() = Stack<'T>(4) @@ -177,7 +177,7 @@ type Queue<'T when 'T: equality> private (initialContents, initialCount) = if initialCapacity < 0 then failwith "capacity is less than 0" - Queue<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + Queue<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Queue<'T>(4) diff --git a/src/fable-library-ts/System.Collections.Generic.fs b/src/fable-library-ts/System.Collections.Generic.fs index 2623852e62..d282f67a14 100644 --- a/src/fable-library-ts/System.Collections.Generic.fs +++ b/src/fable-library-ts/System.Collections.Generic.fs @@ -46,7 +46,7 @@ type Stack<'T> private (initialContents, initialCount) = let mutable contents = initialContents let mutable count = initialCount - new(initialCapacity: int) = Stack<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + new(initialCapacity: int) = Stack<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Stack<'T>(4) @@ -174,7 +174,7 @@ type Queue<'T> private (initialContents, initialCount) = if initialCapacity < 0 then raise (System.ArgumentOutOfRangeException("capacity is less than 0")) - Queue<'T>(Array.zeroCreate<'T> (initialCapacity), 0) + Queue<'T>(Array.zeroCreate<'T>(initialCapacity), 0) new() = Queue<'T>(4) diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index b0b2e3a164..c4c3ebd528 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -56,7 +56,7 @@ type WorkerRequest = | GetCompletionsForFile of id: Guid * file: string * line: int * column: int * lineText: string | GetDeclarationLocationForFile of id: Guid * file: string * line: int * column: int * lineText: string - static member Decoder = Decode.Auto.generateDecoder () + static member Decoder = Decode.Auto.generateDecoder() type CompileStats = { @@ -76,7 +76,7 @@ type WorkerAnswer = | FoundCompletions of id: Guid * Fable.Standalone.Completion[] | FoundDeclarationLocation of id: Guid (* line1, col1, line2, col2 *) * (int * int * int * int) option - static member Decoder = Decode.Auto.generateDecoder () + static member Decoder = Decode.Auto.generateDecoder() type ObservableWorker<'InMsg>(worker: obj, decoder: Decoder<'InMsg>, ?name: string) = let name = defaultArg name "FABLE WORKER" diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 1097937b16..ec5c7dced5 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -29,9 +29,9 @@ let getAssemblyReader (_getBlobUrl: string -> string, _refs: string[]) : JS.Prom let escapeJsStringLiteral (str: string) : string = importMember "./util.js" let measureTime f arg = - let before: float = self?performance?now () + let before: float = self?performance?now() let res = f arg - let after: float = self?performance?now () + let after: float = self?performance?now() res, after - before type PrecompiledState = @@ -291,7 +291,8 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l if not (parseResults.Errors |> Array.exists (fun e -> not e.IsWarning)) then for fileName in filesToEmit do - let! code, fableErrors, transformTime = emitFile fable parseResults fileName language fableOptions + let! code, fableErrors, transformTime = + emitFile fable parseResults fileName language fableOptions compiledCode.Add code errors.AddRange fableErrors @@ -419,7 +420,8 @@ let rec loop (box: MailboxProcessor) (state: State) = else filesToEmit - let! (code, errors, stats) = compileFiles fable names codes filesToEmit language otherFSharpOptions + let! (code, errors, stats) = + compileFiles fable names codes filesToEmit language otherFSharpOptions CompilationsFinished(code, language, errors, stats) |> state.Worker.Post with er -> diff --git a/src/fable-standalone/test/bench-compiler/app.fs b/src/fable-standalone/test/bench-compiler/app.fs index f5997ee673..9e95c18c0f 100644 --- a/src/fable-standalone/test/bench-compiler/app.fs +++ b/src/fable-standalone/test/bench-compiler/app.fs @@ -147,7 +147,7 @@ type SourceWriter(sourcePath, targetPath, projDir, options: CmdLineOptions, file member _.Dispose() = () - member _.SourceMap = mapGenerator.Force().toJSON () + member _.SourceMap = mapGenerator.Force().toJSON() member _.Result = sb.ToString() let printErrors showWarnings (errors: Fable.Standalone.Error[]) = diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index a86f7654c6..7cc7ffee62 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -9,7 +9,7 @@ Fable.Tests - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 471dd3770b..03139f9f9f 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -7,7 +7,7 @@ Preview - + runtime; build; native; contentfiles; analyzers; buildtransitive From eec16db5c6ce2a453e9fa5c21c8377862c321093 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:58:23 +0200 Subject: [PATCH 080/322] chore: Bump @babel/core from 7.29.0 to 7.29.7 in /tests/React (#4977) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 100 +++++++++++++++++----------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index f39e6ef45d..bc3d13674e 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -57,29 +57,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -124,13 +124,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -206,22 +206,22 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2610,9 +2610,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "version": "2.11.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", + "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2634,9 +2634,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz", + "integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", "funding": [ { "type": "opencollective", @@ -2653,11 +2653,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -2953,9 +2953,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.420", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", - "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "version": "1.5.430", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", + "integrity": "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ==", "license": "ISC" }, "node_modules/emittery": { @@ -4565,9 +4565,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "license": "MIT", "engines": { "node": ">=18" @@ -5350,9 +5350,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "funding": [ { "type": "opencollective", From 50a37f78f31394d61349b09a025c1713529d8ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:03:28 -0700 Subject: [PATCH 081/322] fix(rust): Remove reflection registration on boxing (#4983) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 5 +---- src/fable-library-rust/src/Native.rs | 4 ---- src/fable-library-rust/src/Reflection.rs | 18 ++++++------------ src/quicktest-rust/Cargo.toml | 3 ++- tests/Rust/Cargo.toml | 1 + 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 9f6c184b7b..0dd6a3533e 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1872,9 +1872,6 @@ module Util = match argType, arg.Type with | Some Fable.Any, actualType -> match arg, actualType with - | Fable.IdentExpr ident, _ when ident.IsThisArgument && ctx.IsAssocMember -> - let expr = transformLeaveContext com ctx (Some Fable.Any) arg - [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_lrc" | MaybeCasted(Fable.IdentExpr ident), _ when ident.IsThisArgument && ctx.IsAssocMember -> let expr = transformLeaveContext com ctx (Some Fable.Any) arg [ expr |> makeClone ] |> makeLibCall com ctx None "Native" "box_lrc" @@ -3614,7 +3611,7 @@ module Util = (mkInferTy ()) :: (transformGenTypes com ctx [ typ ]) |> mkTypesGenericArgs match fableExpr with - | Fable.IdentExpr ident when isDowncast -> + | MaybeCasted(Fable.IdentExpr ident) when isDowncast -> let downcastExpr = makeLibCall com ctx genArgsOpt "Native" "try_downcast" [ expr ] let pat = makeUnionCasePat (rawIdent "Some") [ makeFullNameIdentPat ident.Name ] mkLetExpr pat downcastExpr diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index e4dd3c6965..32068de059 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -703,7 +703,6 @@ pub mod Native_ { #[cfg(not(feature = "lrc_ptr"))] pub fn box_(x: T) -> LrcPtr { - crate::Reflection_::register_type_name::(); match (&x as &dyn Any).downcast_ref::>() { Some(o) => o.clone(), None => LrcPtr::new(x) as LrcPtr, @@ -712,7 +711,6 @@ pub mod Native_ { #[cfg(feature = "lrc_ptr")] pub fn box_(x: T) -> LrcPtr { - crate::Reflection_::register_type_name::(); match (&x as &dyn Any).downcast_ref::>() { Some(o) => o.clone(), None => LrcPtr::from(Lrc::new(x) as Lrc), @@ -731,7 +729,6 @@ pub mod Native_ { // site) match the pointee's runtime type id for value-based reflection. #[cfg(not(feature = "lrc_ptr"))] pub fn box_lrc(o: LrcPtr) -> LrcPtr { - crate::Reflection_::register_type_name::(); o } @@ -740,7 +737,6 @@ pub mod Native_ { // pointer identity and the pointee's runtime type id are both preserved. #[cfg(feature = "lrc_ptr")] pub fn box_lrc(o: LrcPtr) -> LrcPtr { - crate::Reflection_::register_type_name::(); let inner: Lrc = Lrc::clone(&*o); let coerced: Lrc = inner; LrcPtr::from(coerced) diff --git a/src/fable-library-rust/src/Reflection.rs b/src/fable-library-rust/src/Reflection.rs index 5efba43584..ae1ec58daa 100644 --- a/src/fable-library-rust/src/Reflection.rs +++ b/src/fable-library-rust/src/Reflection.rs @@ -3,7 +3,7 @@ pub mod Reflection_ { use crate::Microsoft::FSharp::Quotations::FSharpPropertyInfo; use crate::NativeArray_::{array_from, Array}; - use crate::Native_::{box_, box_lrc, Any, Func1, LrcPtr, MutCell, OnceInit, Vec}; + use crate::Native_::{box_, box_lrc, unbox_lrc, Any, Func1, LrcPtr, MutCell, OnceInit, Vec}; use crate::String_::string; #[cfg(feature = "no_std")] @@ -215,16 +215,9 @@ pub mod Reflection_ { box_lrc(info) } - fn type_info_of(typ: &obj) -> RecordTypeInfo { - (**typ) - .downcast_ref::() - .expect("Type does not carry record reflection info") - .clone() - } - // FSharpValue.MakeRecord(typ, values, ?bindingFlags) -> obj pub fn makeRecord(typ: obj, values: Array, _flags: Option) -> obj { - let info = type_info_of(&typ); + let info = unbox_lrc::(typ); (info.make)(values) } @@ -246,7 +239,8 @@ pub mod Reflection_ { // FSharpType.GetRecordFields(typ, ?bindingFlags) -> PropertyInfo[] pub fn getRecordElements(typ: obj, _flags: Option) -> Array> { - let info = type_info_of(&typ); + let info = unbox_lrc::(typ); + // registry_insert(info.tid, info.clone()); let props: Vec> = info .fields .get() @@ -258,8 +252,8 @@ pub mod Reflection_ { // FSharpValue.GetRecordFields(record, ?bindingFlags) -> obj[] pub fn getRecordFields(record: obj, typ: obj, _flags: Option) -> Array { - let info = type_info_of(&typ); - registry_insert(info.tid, LrcPtr::new(info.clone())); + let info = unbox_lrc::(typ); + registry_insert(info.tid, info.clone()); let vals: Vec = info .fields .get() diff --git a/src/quicktest-rust/Cargo.toml b/src/quicktest-rust/Cargo.toml index d10bb35820..54606a6189 100644 --- a/src/quicktest-rust/Cargo.toml +++ b/src/quicktest-rust/Cargo.toml @@ -9,7 +9,8 @@ no_std = ["fable_library_rust/no_std"] reflection = ["fable_library_rust/reflection"] static_do_bindings = ["fable_library_rust/static_do_bindings"] threaded = ["fable_library_rust/threaded"] +# default = ["reflection"] # Uncomment to default to "reflection" mode # default = ["threaded"] # Uncomment to default to "threaded" mode [dependencies] -fable_library_rust = { path = "../../temp/fable-library-rust" } +fable_library_rust = { path = "./fable_modules/fable-library-rust" } diff --git a/tests/Rust/Cargo.toml b/tests/Rust/Cargo.toml index d30344b606..fdd2cb5692 100644 --- a/tests/Rust/Cargo.toml +++ b/tests/Rust/Cargo.toml @@ -10,6 +10,7 @@ reflection = ["fable_library_rust/reflection"] static_do_bindings = ["fable_library_rust/static_do_bindings"] threaded = ["fable_library_rust/threaded"] # default = ["no_std"] # Uncomment to default to "no_std" mode +# default = ["reflection"] # Uncomment to default to "reflection" mode # default = ["threaded"] # Uncomment to default to "threaded" mode [dependencies] From aea61b562c461a9b383e805f06f68a3b21ed45c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:21:58 -0700 Subject: [PATCH 082/322] fix(js/ts): Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) --- .../fable_library/MutableMap.fs | 4 +++ .../fable_library/MutableSet.fs | 4 +++ src/fable-library-ts/MutableMap.fs | 4 +++ src/fable-library-ts/MutableSet.fs | 4 +++ tests/Beam/DictionaryTests.fs | 24 +++++++++++++++++ tests/Dart/src/DictionaryTests.fs | 10 +++++++ tests/Js/Main/DictionaryTests.fs | 10 +++++++ tests/Python/TestDictionary.fs | 27 +++++++++++++++++-- tests/Rust/tests/src/DictionaryTests.fs | 9 +++++++ 9 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/fable-library-py/fable_library/MutableMap.fs b/src/fable-library-py/fable_library/MutableMap.fs index 502a283cc8..d3344d27e1 100644 --- a/src/fable-library-py/fable_library/MutableMap.fs +++ b/src/fable-library-py/fable_library/MutableMap.fs @@ -72,6 +72,10 @@ type MutableMap<'Key, 'Value when 'Key: equality> match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/src/fable-library-py/fable_library/MutableSet.fs b/src/fable-library-py/fable_library/MutableSet.fs index c0d329a001..9a886e2bd4 100644 --- a/src/fable-library-py/fable_library/MutableSet.fs +++ b/src/fable-library-py/fable_library/MutableSet.fs @@ -60,6 +60,10 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/src/fable-library-ts/MutableMap.fs b/src/fable-library-ts/MutableMap.fs index 12fb5aee85..04b1633556 100644 --- a/src/fable-library-ts/MutableMap.fs +++ b/src/fable-library-ts/MutableMap.fs @@ -74,6 +74,10 @@ type MutableMap<'Key, 'Value when 'Key: equality> match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/src/fable-library-ts/MutableSet.fs b/src/fable-library-ts/MutableSet.fs index db04abb6a6..100eeb520a 100644 --- a/src/fable-library-ts/MutableSet.fs +++ b/src/fable-library-ts/MutableSet.fs @@ -61,6 +61,10 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer match this.TryFindIndex(k) with | true, h, i when i > -1 -> hashMap.[h].RemoveAt(i) + + if hashMap.[h].Count = 0 then + hashMap.Remove(h) |> ignore + true | _, _, _ -> false diff --git a/tests/Beam/DictionaryTests.fs b/tests/Beam/DictionaryTests.fs index 0694e9ae13..d7cd601aa7 100644 --- a/tests/Beam/DictionaryTests.fs +++ b/tests/Beam/DictionaryTests.fs @@ -5,6 +5,8 @@ open System.Collections.Generic open Fable.Tests.Util open Util.Testing +type MyRecord = { a: int } + // --- Construction --- [] @@ -74,6 +76,28 @@ let ``test Dictionary.Remove works`` () = dic.Remove("C") |> equal false dic.Count |> equal 1 +[] +let ``test Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + +[] +let ``test Dictionary.Remove with records as keys works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let x4 = { a = 15 } + let dict = Dictionary<_, _>() + dict.Add(x1, "Hello") + dict.Add(x3, "World!") + dict.Remove(x2) |> equal true + dict.Remove(x4) |> equal false + dict.Count |> equal 1 + // --- TryGetValue --- [] diff --git a/tests/Dart/src/DictionaryTests.fs b/tests/Dart/src/DictionaryTests.fs index 6addc9e00d..544d1197f6 100644 --- a/tests/Dart/src/DictionaryTests.fs +++ b/tests/Dart/src/DictionaryTests.fs @@ -186,6 +186,15 @@ let tests() = dic.Add("B", "World!") dic.Remove("A") |> equal true dic.Remove("C") |> equal false + dic.Count |> equal 1 + + testCase "Dictionary.Remove with complex keys works" <| fun () -> + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 testCase "Dictionary.Remove with records as keys works" <| fun () -> let x1 = { a = 5 } @@ -197,6 +206,7 @@ let tests() = dic.Add(x3, "World!") dic.Remove(x2) |> equal true dic.Remove(x4) |> equal false + dic.Count |> equal 1 testCase "Interface IDictionary.Count works" <| fun () -> let dic = dict <| seq { for i in 1. .. 10. -> i, i*i } diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index 31037ee512..ea58422792 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -194,6 +194,15 @@ let tests = dic.Add("B", "World!") dic.Remove("A") |> equal true dic.Remove("C") |> equal false + dic.Count |> equal 1 + + testCase "Dictionary.Remove with complex keys works" <| fun () -> + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 testCase "Dictionary.Remove with records as keys works" <| fun () -> let x1 = { a = 5 } @@ -205,6 +214,7 @@ let tests = dic.Add(x3, "World!") dic.Remove(x2) |> equal true dic.Remove(x4) |> equal false + dic.Count |> equal 1 testCase "Interface IDictionary.Count works" <| fun () -> let dic = dict <| seq { for i in 1. .. 10. -> i.ToString(), i*i } diff --git a/tests/Python/TestDictionary.fs b/tests/Python/TestDictionary.fs index 878f652fb8..8f28123c47 100644 --- a/tests/Python/TestDictionary.fs +++ b/tests/Python/TestDictionary.fs @@ -226,16 +226,39 @@ let ``test Dictionary.ContainsValue works`` () = dic.ContainsValue("Everybody!") |> equal false [] -let ``test Dictionary.Delete works`` () = +let ``test Dictionary.Remove works`` () = let dic = Dictionary<_, _>() dic.Add("A", "Hello") dic.Add("B", "World!") dic.ContainsValue("Hello") |> equal true dic.Remove("A") |> equal true dic.ContainsValue("Hello") |> equal false + dic.Count |> equal 1 [] -let ``test Dictionary.Delete works with tuples as keys`` () = +let ``test Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + +[] +let ``test Dictionary.Remove with records as keys works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let x4 = { a = 15 } + let dict = Dictionary<_, _>() + dict.Add(x1, "Hello") + dict.Add(x3, "World!") + dict.Remove(x2) |> equal true + dict.Remove(x4) |> equal false + dict.Count |> equal 1 + +[] +let ``test Dictionary.Remove works with tuples as keys`` () = let my_dict = Dictionary((Map [for i in 0..10 do yield (i,i), sprintf "Number: %i" i])) my_dict.ContainsKey((0,0)) |> equal true my_dict.Remove((0,0)) |> equal true diff --git a/tests/Rust/tests/src/DictionaryTests.fs b/tests/Rust/tests/src/DictionaryTests.fs index 11458d3126..ccd124d79f 100644 --- a/tests/Rust/tests/src/DictionaryTests.fs +++ b/tests/Rust/tests/src/DictionaryTests.fs @@ -271,6 +271,15 @@ let ``Dictionary.Remove works`` () = dict.Remove(3) |> equal false dict.Count |> equal 1 +[] +let ``Dictionary.Remove with complex keys works`` () = + let dic = Dictionary<_, _>() + dic.Add((0,"A"), "Hello") + dic.Add((1,"B"), "World!") + dic.Remove((0,"A")) |> equal true + dic.Remove((1,"B")) |> equal true + dic.Count |> equal 0 + [] let ``Dictionary.Remove with records as keys works`` () = let x1 = { a = 5 } From 3207543d7618019730b814c2cdca8e6e16ad088d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:02:02 +0200 Subject: [PATCH 083/322] chore: Bump the all-pip group with 2 updates (#4988) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 +-- uv.lock | 80 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dfd280e297..4a8b8669df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.7,<0.17", + "ruff>=0.16.8,<0.17", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.5", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.80", + "ty>=0.0.82", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 9fd0e23a62..0966a09892 100644 --- a/uv.lock +++ b/uv.lock @@ -68,8 +68,8 @@ dev = [ { name = "pydantic", specifier = ">=2.13.5" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.7,<0.17" }, - { name = "ty", specifier = ">=0.0.80" }, + { name = "ruff", specifier = ">=0.16.8,<0.17" }, + { name = "ty", specifier = ">=0.0.82" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.7" +version = "0.16.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, - { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, - { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, - { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, - { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, ] [[package]] name = "ty" -version = "0.0.80" +version = "0.0.82" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/b0/6d1b10e0d422736a3c439e487c950ae785f401d71ff879d5be68bccb6d90/ty-0.0.80.tar.gz", hash = "sha256:fe86bc91327e45ff5e3593b7e306e7f57a44bc0608f38d647b8d97bd99c96013", size = 7183998, upload-time = "2026-09-09T21:18:47.547Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/df/b9f35d0b8860f1bbfcb852fbef363301b5ca4b5590cadc6f2a810646d579/ty-0.0.82.tar.gz", hash = "sha256:586e3bf784cece42113929bb64b4761bed5c2127ae6cea294bb82da670066356", size = 7325958, upload-time = "2026-09-17T19:42:12.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/c8/3c93195eca282936ebb574d0ba98843e4b147ee324006f88464777012a92/ty-0.0.80-py3-none-linux_armv6l.whl", hash = "sha256:738d1cfca466c577aea24547c348629c00c63548cf7da2c46b497b3840f85dee", size = 13606509, upload-time = "2026-09-09T21:18:10.476Z" }, - { url = "https://files.pythonhosted.org/packages/e7/48/bbb47f7001c97262a5109bcd92a45bcc8a2fbb21e994c214a9897630bfb7/ty-0.0.80-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:56060164bb8ee43770fa367524fbdba2611fc90f8923a02cc91f80eb37d5a1b0", size = 13203812, upload-time = "2026-09-09T21:18:12.796Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/fd141160567047b742e466be8ed09a4c4ed80fa045d37f946eb4f4532fee/ty-0.0.80-py3-none-macosx_11_0_arm64.whl", hash = "sha256:da4062e0fbf3923d9b71688157c5753394f243348f00732e9edbb27498397169", size = 13021896, upload-time = "2026-09-09T21:18:15.186Z" }, - { url = "https://files.pythonhosted.org/packages/1e/10/b4177faf9e71bc37bc4d08f512042269660a1ce0ec457c48f96e51fc91d7/ty-0.0.80-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9eb94b2659f506a3a07a9ea1415d5c18aaa1e554adc1612614d617a0ed320e1", size = 13083881, upload-time = "2026-09-09T21:18:17.616Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c2/192d48b9a9acbbe030fc9a4bd73af75671a5566e731949f77e912a68ce68/ty-0.0.80-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2f5e39a87da48c1af1a3b3135f02be7a83a93da30b0d6a5f5d27806e7c747ce", size = 13354874, upload-time = "2026-09-09T21:18:19.744Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/736dfd31efd98bed9dbe018ff91676e5ea83485b1e24afb635fd0247728f/ty-0.0.80-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e00149e7779c6b98f3ba12e10a631a861bd7ca1a42bbf064e2677ada7d2c0c2", size = 14204805, upload-time = "2026-09-09T21:18:21.999Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6f/557c726cb53401998e3589bb632c963a6887a70240e0a4386024f3731c81/ty-0.0.80-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a377268f359fb6e7a2cc9026a09223c764f58d020dec8e6baeaba9caba6ff829", size = 14630014, upload-time = "2026-09-09T21:18:24Z" }, - { url = "https://files.pythonhosted.org/packages/e9/19/7a4b18fe27f6b6bd4ffa56b67c8b09ecb76bb4312d1c64b73f65417b4dbd/ty-0.0.80-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4511dbf1b266b9ce5b73e9b28d94468096c2b6da00e2fe205168c32d3b352ca7", size = 14326946, upload-time = "2026-09-09T21:18:26.679Z" }, - { url = "https://files.pythonhosted.org/packages/f8/95/16dd90805fc7e53ec54ae9fdb1a8b74753fb159a55a170f60f49b9417824/ty-0.0.80-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe95feffa7156800c6f804195acb9fb5846a39671c7192c30fff23351aaf7c31", size = 13705988, upload-time = "2026-09-09T21:18:28.681Z" }, - { url = "https://files.pythonhosted.org/packages/b1/09/4e87992c23ab8a742c7d4914ac5fe66fb9301c8464a69fd2ecbc0be3fbcd/ty-0.0.80-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba26b39f06bc8c3c2c5acd3a147239482b36620cdbca1b9d02e915294854f2cc", size = 14233729, upload-time = "2026-09-09T21:18:30.961Z" }, - { url = "https://files.pythonhosted.org/packages/36/e9/b8c11fda8e66a1d1cc1a7160ed719a33f4dc0107b1cef6a14e2673965763/ty-0.0.80-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2e6167320888c115a6fbe69893b63fd1c848f9121a454e75e5f612674528e3f3", size = 13173523, upload-time = "2026-09-09T21:18:33.266Z" }, - { url = "https://files.pythonhosted.org/packages/73/a7/512083fa540c5be1ac8642658f03bfb5bfa6fd168ea4f55e3c0aabe04166/ty-0.0.80-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7aecb62b4de70b479eab07d1779ea66da26dd8b068f50a9330867157312f103c", size = 13373014, upload-time = "2026-09-09T21:18:35.339Z" }, - { url = "https://files.pythonhosted.org/packages/5e/63/cd2f0ca81fd9b8cafef93bbcf022bf636bc4de8ee7465c6aafeec1d4d52b/ty-0.0.80-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4fed06adacd16b7e2d722f37449021b1419598d0440769701c0f4827faddde63", size = 13667827, upload-time = "2026-09-09T21:18:37.241Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5d/ebdfdcb2dc099ae74dc4b75511eef0dd398b2fa27006c543543974b4567f/ty-0.0.80-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:09329af6303ce611ec2cfffc995dc1ddf76c9f47194fc1a4d64a984759c25f5b", size = 13963866, upload-time = "2026-09-09T21:18:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/c286d5e1b560cfd16d6e91fdab683718aacf09fc88938ca06dcc9d1c8502/ty-0.0.80-py3-none-win32.whl", hash = "sha256:a81b3b512f7b4c68e42fbd1835633de658809666e9aafd5defa52bbc5197698d", size = 12881806, upload-time = "2026-09-09T21:18:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c8/3b3a8ac16d47a23ff34bf57c0d99a491860748aea96e0e886b4bfed54f9b/ty-0.0.80-py3-none-win_amd64.whl", hash = "sha256:8043f99878a2ae434781cb881c8a3840dff70c4a5b60bdc5ae84251f35ee063f", size = 13528883, upload-time = "2026-09-09T21:18:43.687Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/a6c697930fca76596d7d17f8853120f41a298fd9ea632c901cfca8ca869b/ty-0.0.80-py3-none-win_arm64.whl", hash = "sha256:e277ef034331da5319efc839c064968d24f35715b71c70369cf97d36fe4dcbdb", size = 13370135, upload-time = "2026-09-09T21:18:45.758Z" }, + { url = "https://files.pythonhosted.org/packages/31/0e/259760638b7b1a875743f1f3975253392932ff74397c8e174df7bfb143c3/ty-0.0.82-py3-none-linux_armv6l.whl", hash = "sha256:be58e089e317e1ada7a655216828e76d369386dc958f7a3b3d455ab6dc7f02b6", size = 13833734, upload-time = "2026-09-17T19:41:35.134Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/c8a1f70989cf44cf2cada5527e268b4216015a7f28f1d8817e4ef1e84a52/ty-0.0.82-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c080ba9a816cfbb3e34c0d3a0071829c602d14bb992bc799d885d4b30008b4ab", size = 13449689, upload-time = "2026-09-17T19:41:37.65Z" }, + { url = "https://files.pythonhosted.org/packages/d5/eb/31fc4bdecb2ff0053443d4ec1e6719a3f3171636ad6da7a8a642c879a4f4/ty-0.0.82-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f08bd5b751c3b2c46eabe8aee890e7fa54e3bcd97ffa4b8338b0a2508d928c2", size = 13410140, upload-time = "2026-09-17T19:41:40.024Z" }, + { url = "https://files.pythonhosted.org/packages/e7/14/e2af8c64004cd891691915b1522f105a5d601dd8d76a635fec112911f251/ty-0.0.82-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f5ef0401743c2eeac8039ef9ff1bc5dd612bd17ed0259b9b0794e19696548a6", size = 13435552, upload-time = "2026-09-17T19:41:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/b9519a7854189aa8da4309ecb9c6ff5ab8ac91a433e201322b15c98f9388/ty-0.0.82-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c899643f2a4c1782c06a4bf4959909d1aa2d9d2cac7c1ac3de3161dfcb6909f8", size = 13601427, upload-time = "2026-09-17T19:41:44.242Z" }, + { url = "https://files.pythonhosted.org/packages/ce/be/0c7a39e277a25bca21bbd2dcf640eb4d80cf98f318ef611092cda1222399/ty-0.0.82-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aa6e4e157c6e0b770c327a5d966371b2a7bc7ad4f22d662ac068623cba00eb", size = 14457370, upload-time = "2026-09-17T19:41:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/6f/33/86ff8f1e15c71096838eb0fdfe0f5b578dc717575de6cba591d96baf3b06/ty-0.0.82-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28c141ab481748f1b889eb572e803172ea452af621e514c30a2f30884753b020", size = 14888674, upload-time = "2026-09-17T19:41:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d2/a1c6315907afd080b4e98d2972441b4803a70c9663a8c7a7f6fd554c7361/ty-0.0.82-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16fff2729fbaa3998ba1cab536dc7d176aef28927b09200588e50982394c56df", size = 14670369, upload-time = "2026-09-17T19:41:50.963Z" }, + { url = "https://files.pythonhosted.org/packages/ae/cd/956ceae4dbe0b116ebc9ecb75dcbe9a0294c50146f76c4c59004e6eb2d77/ty-0.0.82-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2024a33209b37525ffa85a29036985e9ef53a013c0ff321231bd8b23987cf71", size = 14107030, upload-time = "2026-09-17T19:41:53.343Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f8/6025627cec4d6370fb34a2c5cdcf36d89dd4e4ac75b023bda68231e17dfc/ty-0.0.82-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14a0ae8591e08aca5911b545d100e51c9b796350a85b2b36d82219820552d662", size = 14485892, upload-time = "2026-09-17T19:41:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b6/016f9b36096a9ccd0492ee000380bd75fb51dc4ec7bfef4d6d93f6b3ec2e/ty-0.0.82-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2964202b09108c72232760119c2680cefc33191c428b09e6f620b6c0ab9c6b0f", size = 13383907, upload-time = "2026-09-17T19:41:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/19/1ccc2429399d6ef3d98180340c7abc6793bd90c5039ad41a9003cb8de6df/ty-0.0.82-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f4ed8fc714e35dc11ef4209832e73ad461cf29c9161099eb6c2c541c6b9a3e0e", size = 13617005, upload-time = "2026-09-17T19:42:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9d/a4a487db72e78413bba2ee9cd49f79b8f461be93e7f6c4dd790febae1cd7/ty-0.0.82-py3-none-musllinux_1_2_i686.whl", hash = "sha256:32a3fe7b1a50f20679fe76c85c0087280419365d900300898a7b95cbcfe510fc", size = 13906545, upload-time = "2026-09-17T19:42:01.942Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4e/a58d530c0f5abe7b846e2d05af9e3f1fb44a1b620ae0abe3892cd3f9596c/ty-0.0.82-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f751044ae63ea9e73ce2eae4c23a839efa4a04d6487d6ea411a590fbe43360b6", size = 14248518, upload-time = "2026-09-17T19:42:04.164Z" }, + { url = "https://files.pythonhosted.org/packages/a6/db/5505229835171d19adcaaeec3a15e0b533b578d24de1329db5da9306c4c6/ty-0.0.82-py3-none-win32.whl", hash = "sha256:0eb66c5ce58434a48d2cae2780d25285c7ca19e07a0d38b1fe39d1a0f75d6fa3", size = 13059890, upload-time = "2026-09-17T19:42:06.161Z" }, + { url = "https://files.pythonhosted.org/packages/13/22/faa427b65baf39915f52a6fa079ece4253e831f3364c567bf91bc1eef65a/ty-0.0.82-py3-none-win_amd64.whl", hash = "sha256:1e4a6c1ca2dc1a6929517d73a47cd626989bb8bd67811425fbe022d06eb2bf73", size = 14003730, upload-time = "2026-09-17T19:42:08.268Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fc/3b95100993d28acb2f3bcf3a49846985f5b2aafab6562627d018f6d23788/ty-0.0.82-py3-none-win_arm64.whl", hash = "sha256:a6f0e8a1f961518746edbccc5986945c01a56a917ebe37117aa028a038d54ae3", size = 13567713, upload-time = "2026-09-17T19:42:10.248Z" }, ] [[package]] From 92270c66cab71f307ebb61e8a69676c8d913ffdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:03:30 +0200 Subject: [PATCH 084/322] chore: Bump the all-actions group with 2 updates (#4989) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e07b9a9d21..632e6e356a 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup-cli@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 5fa84fbe48..04ba0a95f4 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@34ac5c19ea597a7fc734abbd19d754f1f9a95a87 # v0.89.13 + uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 0a261f662e6b1545fd64fc6565afbf5f66ac45f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:04:25 +0200 Subject: [PATCH 085/322] chore: Bump the all-npm group in /tests/React with 2 updates (#4986) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 371 +++++++++++++++++----------------- 1 file changed, 186 insertions(+), 185 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index bc3d13674e..41ac80119c 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -1035,9 +1035,9 @@ } }, "node_modules/@jest/console": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.1.tgz", - "integrity": "sha512-u5Ncuc+gXVUwjNMFQOnphHo2Qx2DxyC8Dvpmf4HCx4y0kvSkRRNiQYRixrvOP4V7y52JcSuNxqochCt0PpdHVg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz", + "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==", "license": "MIT", "dependencies": { "@jest/types": "30.5.1", @@ -1052,16 +1052,16 @@ } }, "node_modules/@jest/core": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.1.tgz", - "integrity": "sha512-BL9g6CJUUhIbdoAflz/Va658erSSXUIvU8XUYiWNTbZljJjZ5yaC9EJ9/dQ19rpbYV3ZOK4sBACqhPaTyhoUIA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz", + "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.1", + "@jest/console": "30.5.2", "@jest/pattern": "30.5.0", - "@jest/reporters": "30.5.1", - "@jest/test-result": "30.5.1", - "@jest/transform": "30.5.1", + "@jest/reporters": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -1071,18 +1071,18 @@ "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", "jest-changed-files": "30.5.1", - "jest-config": "30.5.1", + "jest-config": "30.5.2", "jest-haste-map": "30.5.1", "jest-message-util": "30.5.1", "jest-regex-util": "30.5.0", "jest-resolve": "30.5.1", - "jest-resolve-dependencies": "30.5.1", - "jest-runner": "30.5.1", - "jest-runtime": "30.5.1", - "jest-snapshot": "30.5.1", + "jest-resolve-dependencies": "30.5.2", + "jest-runner": "30.5.2", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", "jest-util": "30.5.1", "jest-validate": "30.5.1", - "jest-watcher": "30.5.1", + "jest-watcher": "30.5.2", "pretty-format": "30.5.1", "slash": "^3.0.0" }, @@ -1135,31 +1135,31 @@ } }, "node_modules/@jest/environment": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.1.tgz", - "integrity": "sha512-eYJAkOsrpwDXPcoLNEG6lN9Zo3Cy35pxnVQD74vyIfsi/Q8wB/lZFsEjU4wrecOgT4ZviQDZaX/cFIJyMdMxTw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz", + "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==", "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.5.1", + "@jest/fake-timers": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.1" + "jest-mock": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.1.tgz", - "integrity": "sha512-J395vmP3Fb2Te0JmF7pe4si4jpfbXef1YsY4UpHYL6OOxS2molu9Dsie1VmIiUalXdtmz1P5QRgc+5hBD+ssBg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.5.2.tgz", + "integrity": "sha512-8/P+8hKv7L4mhFoB6gpYLj/fuzXk8PWrde7TDHh8tJz5pE1ktlMbg0y6hDY/V8MfAnispFlPl/vPifowLy3JGQ==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/fake-timers": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.1", + "jest-mock": "30.5.2", "jest-util": "30.5.1" }, "engines": { @@ -1177,22 +1177,22 @@ } }, "node_modules/@jest/expect": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.1.tgz", - "integrity": "sha512-uOGd40P/COyUp9xHf5jeGiGJC2/ANg+2+Tk9/xN5/LxmlY/r/gxsPrx3DTEtaRZsLAX4wgNSLEDELZ5bmVs2bA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==", "license": "MIT", "dependencies": { - "expect": "30.5.1", - "jest-snapshot": "30.5.1" + "expect": "30.5.2", + "jest-snapshot": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.1.tgz", - "integrity": "sha512-WcRWhHQdTMRDpyWKZ/6MINBmovI7zeD+bL8wFjCncRV3NQOwKy1X45IfyblfHR4k/XciIlNEdFL9QjFO+HNKOg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz", + "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0" @@ -1202,16 +1202,16 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.1.tgz", - "integrity": "sha512-rEkV6YzpBXo/L9cnj2ibyuYZuyGiNWaPUsyCu3HYJbqCV4jnEiKmf7hId20z8eKjZ0JQ9jtIYKxRSmYB/OYSsA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz", + "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==", "license": "MIT", "dependencies": { "@jest/types": "30.5.1", "@sinonjs/fake-timers": "^15.4.0", "@types/node": "*", "jest-message-util": "30.5.1", - "jest-mock": "30.5.1", + "jest-mock": "30.5.2", "jest-util": "30.5.1" }, "engines": { @@ -1228,15 +1228,15 @@ } }, "node_modules/@jest/globals": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.1.tgz", - "integrity": "sha512-VhqvQ251XIC7pk46YymI3HCeBLOdvriDw9zibekodAHEwoVvbVVgpU5H13GvmyOAzxtZCfsm1uvzqkaqJf2I+g==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz", + "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/expect": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", "@jest/types": "30.5.1", - "jest-mock": "30.5.1" + "jest-mock": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1270,15 +1270,15 @@ "license": "MIT" }, "node_modules/@jest/reporters": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.1.tgz", - "integrity": "sha512-RbUXIfv85KxitJn4l3MpAoMilkvXe2QCO5lXxHWftywo/VdZ5vkEoHRDgphcxP6qmoIkUaN8/UZj6NhdkIFdJg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz", + "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==", "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.5.1", - "@jest/test-result": "30.5.1", - "@jest/transform": "30.5.1", + "@jest/console": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", "@jest/types": "30.5.1", "@jridgewell/trace-mapping": "^0.3.31", "@types/node": "*", @@ -1339,9 +1339,9 @@ } }, "node_modules/@jest/source-map": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.0.tgz", - "integrity": "sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz", + "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", @@ -1354,12 +1354,12 @@ } }, "node_modules/@jest/test-result": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.1.tgz", - "integrity": "sha512-A/1S6ZBdpic50E0pxLgvaB9XNPL4k7AksmG69OO2oiotxciWZwHkbOec+qbIi+uUtIIByOVlXJUl97oZUsz+Jw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz", + "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.1", + "@jest/console": "30.5.2", "@jest/types": "30.5.1", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" @@ -1369,12 +1369,12 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.1.tgz", - "integrity": "sha512-SHcPnrjdVRYJv6y6l4JUTy4Jccu7zmO6BmiOfOA34UiVBto22s19WiDC0A/2qfM7MWB/qdcoqfSIRMitpjTT4A==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz", + "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.5.1", + "@jest/test-result": "30.5.2", "graceful-fs": "^4.2.11", "jest-haste-map": "30.5.1", "slash": "^3.0.0" @@ -1384,9 +1384,9 @@ } }, "node_modules/@jest/transform": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.1.tgz", - "integrity": "sha512-EDnDhn0jleU9ZhpVoA4gvqw+Ev0iw/r5upNT2b79RiwiaTiYAMMhvNJP3lmocjIe5J2ZkoNr0B3K/J6O5GIm4Q==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz", + "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -1478,9 +1478,9 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", - "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", "license": "MIT", "optional": true, "dependencies": { @@ -1940,9 +1940,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", "license": "MIT", "optional": true, "dependencies": { @@ -2507,12 +2507,12 @@ } }, "node_modules/babel-jest": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.1.tgz", - "integrity": "sha512-ge1xUVZS91ml09YRMgRGgeKJ4YJpcOiuwteAxFBYLugQyp7cRw+hHej6Ho0vPjvLrjq60bb7JPHH9LAMA1/krA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz", + "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==", "license": "MIT", "dependencies": { - "@jest/transform": "30.5.1", + "@jest/transform": "30.5.2", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^8.0.0", "babel-preset-jest": "30.5.0", @@ -2622,9 +2622,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3067,16 +3067,16 @@ } }, "node_modules/expect": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.1.tgz", - "integrity": "sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.5.1", + "@jest/expect-utils": "30.5.2", "@jest/get-type": "30.5.0", - "jest-matcher-utils": "30.5.1", + "jest-matcher-utils": "30.5.2", "jest-message-util": "30.5.1", - "jest-mock": "30.5.1", + "jest-mock": "30.5.2", "jest-util": "30.5.1" }, "engines": { @@ -3492,15 +3492,15 @@ } }, "node_modules/jest": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.1.tgz", - "integrity": "sha512-3qrR8+ZXFnn7y0H2yjWQNkGGBLBY4zTRoTMAq9zJcgwLLtlyonfsCLviIXK9xuE2KgIyM+M36AFcoK2DgFR36w==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz", + "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==", "license": "MIT", "dependencies": { - "@jest/core": "30.5.1", + "@jest/core": "30.5.2", "@jest/types": "30.5.1", "import-local": "^3.2.0", - "jest-cli": "30.5.1" + "jest-cli": "30.5.2" }, "bin": { "jest": "bin/jest.js" @@ -3532,25 +3532,25 @@ } }, "node_modules/jest-circus": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.1.tgz", - "integrity": "sha512-NgliezXQ6yznqR4W5Gqw++0cZSbBZlF0NknNIAE5VmSJKWOtmWJmWnBq3TSkUOVBTPrT7FGGhVdA8DLWnxo2Sw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz", + "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/expect": "30.5.1", - "@jest/test-result": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/expect": "30.5.2", + "@jest/test-result": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.5.1", - "jest-matcher-utils": "30.5.1", + "jest-each": "30.5.2", + "jest-matcher-utils": "30.5.2", "jest-message-util": "30.5.1", - "jest-runtime": "30.5.1", - "jest-snapshot": "30.5.1", + "jest-runtime": "30.5.2", + "jest-snapshot": "30.5.2", "jest-util": "30.5.1", "p-limit": "^3.1.0", "pretty-format": "30.5.1", @@ -3590,18 +3590,18 @@ } }, "node_modules/jest-cli": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.1.tgz", - "integrity": "sha512-uwNYepWgaNBCplm42fCIUZTf/tIEHIy5AYvx7eL1BrwIgyjPt+2poouR23UO2yopIP+kV8oZFHfv+l4pg/8prQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz", + "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==", "license": "MIT", "dependencies": { - "@jest/core": "30.5.1", - "@jest/test-result": "30.5.1", + "@jest/core": "30.5.2", + "@jest/test-result": "30.5.2", "@jest/types": "30.5.1", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.5.1", + "jest-config": "30.5.2", "jest-util": "30.5.1", "jest-validate": "30.5.1", "yargs": "^17.7.2" @@ -3622,28 +3622,28 @@ } }, "node_modules/jest-config": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.1.tgz", - "integrity": "sha512-L8PKM2X/ngG8PxfLMqglKGZjylPgw84bVPUlMY9W/o76TnLqPWrmQgsaT0HrheGdRtpGE5FbmREA0Kvb+Qx5gQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz", + "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.5.0", "@jest/pattern": "30.5.0", - "@jest/test-sequencer": "30.5.1", + "@jest/test-sequencer": "30.5.2", "@jest/types": "30.5.1", - "babel-jest": "30.5.1", + "babel-jest": "30.5.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^13.0.6", "graceful-fs": "^4.2.11", - "jest-circus": "30.5.1", + "jest-circus": "30.5.2", "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.1", + "jest-environment-node": "30.5.2", "jest-regex-util": "30.5.0", "jest-resolve": "30.5.1", - "jest-runner": "30.5.1", + "jest-runner": "30.5.2", "jest-util": "30.5.1", "jest-validate": "30.5.1", "parse-json": "^5.2.0", @@ -3699,9 +3699,9 @@ } }, "node_modules/jest-diff": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.1.tgz", - "integrity": "sha512-e3cNNMpv8Kh20MjjphTXs+3Vz7DQyLM1nft7KJhnh46atFhjVJRa+0Hq0beywuwsACtMQUBihQkFl8zxb7gt1Q==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz", + "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==", "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.5.0", @@ -3753,14 +3753,15 @@ } }, "node_modules/jest-each": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.1.tgz", - "integrity": "sha512-S1af0TU4v1EZ/AUlkFs/sxf/5KGsbAT9kRgdyoMX/x72y7C8ZEgETE5o1TPnbKRnrbprc5FR/moYLfdRmqEjsQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz", + "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", "@jest/types": "30.5.1", "chalk": "^4.1.2", + "jest-regex-util": "30.5.0", "jest-util": "30.5.1", "pretty-format": "30.5.1" }, @@ -3796,13 +3797,13 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.1.tgz", - "integrity": "sha512-8lzKbC/SRbQE24wr1OOJV+aYtDAuVNKBryN6YcFiCcaZZ3I7grcZY7w91BwNvGET0ubKDmomEHZFHMaF+6pAlA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.5.2.tgz", + "integrity": "sha512-6NOZjEP+WNi4WXjbxb/1ROnfaMxn6Dhqu6uSC22o0H28z+uppejs/jZ9KCVoLepqqoCjcNZujtzQqC9jSV15jg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/environment-jsdom-abstract": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/environment-jsdom-abstract": "30.5.2", "@types/jsdom": "^21.1.7", "jsdom": "^26.1.0" }, @@ -3819,16 +3820,16 @@ } }, "node_modules/jest-environment-node": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.1.tgz", - "integrity": "sha512-LrPj3sPMjsQoOB3jrb8p/sa+XkSFNKo60TTsyc+EB2kxQJHgbwElpXnx1yX25fdFn5958FIObRtcLSHyV8VIAw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz", + "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/fake-timers": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", - "jest-mock": "30.5.1", + "jest-mock": "30.5.2", "jest-util": "30.5.1", "jest-validate": "30.5.1" }, @@ -3899,14 +3900,14 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.1.tgz", - "integrity": "sha512-aroZVqwOz/wC2y6pC+obgFWKV9viaQWQTTSB6W5H55+egtUczIfXR/rxExTv92xD/ADYwpqaYfVWx1aqwKg7FA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz", + "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==", "license": "MIT", "dependencies": { "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "jest-diff": "30.5.1", + "jest-diff": "30.5.2", "pretty-format": "30.5.1" }, "engines": { @@ -3989,12 +3990,12 @@ } }, "node_modules/jest-mock": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.1.tgz", - "integrity": "sha512-9fVjc3leUpGID2/by/LU4Dvdcp7PFh9LlxS3QRWK3ABm+KtvEVsG/AEGeLY3gKOZsjkBxyfwGltoAVlW7dygHg==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz", + "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==", "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.5.1", + "@jest/expect-utils": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "jest-util": "30.5.1" @@ -4031,29 +4032,29 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.1.tgz", - "integrity": "sha512-JKpXGONDcTaunrNVn8KCl6qAnwl06jIkvv70lhq0Ze47lsPx9sH5HoeEUiXX6iFGnliHN/qpIbQ0l38wrVmXaw==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz", + "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==", "license": "MIT", "dependencies": { "jest-regex-util": "30.5.0", - "jest-snapshot": "30.5.1" + "jest-snapshot": "30.5.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.1.tgz", - "integrity": "sha512-FPlQE4+mwnFxXmpPrSi836KV2ZzvK1g6/nPCT8o5BcoDUUNJQeHo1/Qdkoe/QeoL4M7OeJpbnGUhYKhC1VMdaQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz", + "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==", "license": "MIT", "dependencies": { - "@jest/console": "30.5.1", - "@jest/environment": "30.5.1", - "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.1", - "@jest/transform": "30.5.1", + "@jest/console": "30.5.2", + "@jest/environment": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", @@ -4061,14 +4062,14 @@ "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.1", + "jest-environment-node": "30.5.2", "jest-haste-map": "30.5.1", "jest-leak-detector": "30.5.1", "jest-message-util": "30.5.1", "jest-resolve": "30.5.1", - "jest-runtime": "30.5.1", + "jest-runtime": "30.5.2", "jest-util": "30.5.1", - "jest-watcher": "30.5.1", + "jest-watcher": "30.5.2", "jest-worker": "30.5.1", "p-limit": "^3.1.0" }, @@ -4077,17 +4078,17 @@ } }, "node_modules/jest-runtime": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.1.tgz", - "integrity": "sha512-UB88+NRkK2Tw/OqV7dofYcyiUGrVZtD41k/N0xQOs9fG//57XHK7JLWG52HD1UYpUAhjK4xm6DBvFX3yWudsMA==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz", + "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==", "license": "MIT", "dependencies": { - "@jest/environment": "30.5.1", - "@jest/fake-timers": "30.5.1", - "@jest/globals": "30.5.1", - "@jest/source-map": "30.5.0", - "@jest/test-result": "30.5.1", - "@jest/transform": "30.5.1", + "@jest/environment": "30.5.2", + "@jest/fake-timers": "30.5.2", + "@jest/globals": "30.5.2", + "@jest/source-map": "30.5.2", + "@jest/test-result": "30.5.2", + "@jest/transform": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", @@ -4098,10 +4099,10 @@ "graceful-fs": "^4.2.11", "jest-haste-map": "30.5.1", "jest-message-util": "30.5.1", - "jest-mock": "30.5.1", + "jest-mock": "30.5.2", "jest-regex-util": "30.5.0", "jest-resolve": "30.5.1", - "jest-snapshot": "30.5.1", + "jest-snapshot": "30.5.2", "jest-util": "30.5.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" @@ -4111,9 +4112,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.1.tgz", - "integrity": "sha512-cNWFdSb5xuDGl8hKkAZJ3YtI/PzHpAPFV+HUXWIOG8rMhpDTLVbvAc2d2wRicoYw2wGsJGLaurJT6BLC97bLXQ==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz", + "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==", "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", @@ -4121,17 +4122,17 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.5.1", + "@jest/expect-utils": "30.5.2", "@jest/get-type": "30.5.0", "@jest/snapshot-utils": "30.5.1", - "@jest/transform": "30.5.1", + "@jest/transform": "30.5.2", "@jest/types": "30.5.1", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.5.1", + "expect": "30.5.2", "graceful-fs": "^4.2.11", - "jest-diff": "30.5.1", - "jest-matcher-utils": "30.5.1", + "jest-diff": "30.5.2", + "jest-matcher-utils": "30.5.2", "jest-message-util": "30.5.1", "jest-util": "30.5.1", "pretty-format": "30.5.1", @@ -4255,12 +4256,12 @@ } }, "node_modules/jest-watcher": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.1.tgz", - "integrity": "sha512-+FHJ7C+S7b3ySfhA1aFmoa6TztsnwZVv84ycPRn0tVN93np2EU4b8C/KadqA3l6igdjgLBTnUotab9ER0HLG8A==", + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz", + "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==", "license": "MIT", "dependencies": { - "@jest/test-result": "30.5.1", + "@jest/test-result": "30.5.2", "@jest/types": "30.5.1", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -4595,9 +4596,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.27", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz", - "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==", + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", "license": "MIT" }, "node_modules/onetime": { @@ -4737,9 +4738,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5171,9 +5172,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" From deaf5b215fa6fcc0bd5e64d0219c3c162945c454 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:05:16 +0200 Subject: [PATCH 086/322] chore: Bump the all-npm group with 2 updates (#4987) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 216 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index 817dcb1713..c0940a2504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.5.1", + "@types/node": "^26.6.2", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.2", + "rollup": "^4.63.4", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.2.tgz", - "integrity": "sha512-Xa6RDoWa+hNiX6PgsljlH6W75RaONx3y6PVlbLhkEWW+GaPQ3dP5gwbL/erAzQHWwkvW5UxdD5l87Qx2FAQ/4A==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.2.tgz", - "integrity": "sha512-vNASxsghMfQ5s+v3PrpnJd+ryL/26lxCCaGI+sDJ7VzmHiYXIrrVltsDhaawxLM1WcoMU2oYlbPHLaYQtBzhcg==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.2.tgz", - "integrity": "sha512-0dWDjmlrpZAgjPD/aPzUDhBW8APLRjAni5bOrM76wiiZm+E+KTMVKNhAzaTBohz8UyO2fKNAl0+fygbe2HZXOA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.2.tgz", - "integrity": "sha512-N58uktcwzk3+qT4KHEuNdIxX1N01RWrkfVoml69EAbSaNDL+sbNVLx2RMl4Qd23lpA0fgPvyh5hHb4weD5WKmg==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.2.tgz", - "integrity": "sha512-HWF2zH8EAp2scWRpt2PGe6iUGz7zi04waXsdRr3zb4DWCk2ImIo5FZu0jjmD53nP/DGSvnW0e7/1ToCNZs2lZw==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.2.tgz", - "integrity": "sha512-MkvcwHMnzPSMOQEwB6wHnLzmc+hT8BGc5bW/Mhmjjgx3wbj6VBnlc47XsK74kD0K9MikFfXpQqyz4NUXaUW62A==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.2.tgz", - "integrity": "sha512-xe1bCKPJaKsD0tfd7Rb6bGfUogJTpKbTEEthsfdb7hTfTRNJVQTdirabQx0o6ERVba/smkM720soMY+0QnrlSQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.2.tgz", - "integrity": "sha512-yOM7LdK0p6gk6+Q773OEwtlsikT1TL3yMmYsTtRlDRPha5vV2DC5x7LqRWDr6f3cSYNMKVqxzffXv8ivxNBIFQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.2.tgz", - "integrity": "sha512-qiWuJJV3DybA2IfzvRimeKXGrGuVPv1zobSY/26KnP3HbV0VcNb3ECzgvtbvF3xjSMkcooou6HASXZuLdjnhpQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.2.tgz", - "integrity": "sha512-akcZquRzCY/KpUoZAMBhGf7oi4LmXq1BzRA5CPAC3rkUf28Y/sAYV3jSL+JKd7cwEyFvR5G0XVZ0gaMedP+60A==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.2.tgz", - "integrity": "sha512-fNwYHrPyYyxauPzX/cpYw8Z7LQpp+DGA0KCoswA0aVFBpmdMil9XgjB8V3Ny64Ihu797+GKcuJqnsOKEmor7fA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.2.tgz", - "integrity": "sha512-XfvsgzR7DZqREdst7K1Mj3ilSUM5xLAHJcIMDFPKdxTs9q5VHOT8aMA+a683fqBu7DQl8+Sd9HCsQYL8EMY9qA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.2.tgz", - "integrity": "sha512-Pp7gVZggEFlbcuztay+/U0gVG9S1XAh8i7I1Re/htbAzo43P5wHZHw6pTyzotISqlKohoh9RpIfnOz3RbemK1w==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.2.tgz", - "integrity": "sha512-zkgL2xff6i7u5hau/m6FGeS8gRkLEdgLw522WGmdWWlLd9btmNl3S80mcEjtGq+kvgUekQ3+BOYLLLcPlS2LIA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.2.tgz", - "integrity": "sha512-qOheJomrkVCbbHFJ7L3J97cnhfogKqguAQphv26+3ZsAQIF1L19b+dArl//s8rjJHJLz9byykyM8NBP4nmSa1g==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.2.tgz", - "integrity": "sha512-XlxLD54wQhH3FciCgMofxBw27NzUe818gJH410qWvc41UT0ZFcgxVjyX5/EK8MPTupjeVWqN5oy+9pCA9mqfCA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.2.tgz", - "integrity": "sha512-vdryWeRb2bLJZf0Fv/W8se6nvsHe2PkTCxV0meheK3nQE+G90VCJcke51Miy1yQRsfm2uqIyjXOu4wmUzbTtkQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.2.tgz", - "integrity": "sha512-bcq2h2pkKmH2po4cZV8VWzO4lL40STyu/nLoFpYMQp9C2tCVNTdcVv86MwSsn3D5s1FBe2Ty1atqvVAUTMimNg==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.2.tgz", - "integrity": "sha512-EGoo5DMVMRkTId8fuTDaoxVlR5ZTsKULUezRjd9gCw5eeY+DjCvDpZAOlNUvKPGX+7rS1RWx6j+yOpNPx0cUgQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.2.tgz", - "integrity": "sha512-MErl12k7BFHZG1TI9QF/3lSSZARzq9KgNy/FjnqFMCkv+N4RSSzoUCA5h2mqHX4Mox3WaTVKblyzhQ1zRb2ZuQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.2.tgz", - "integrity": "sha512-ILs8k07Wh4p0PsNY4wYLEaXZKMOpVhrG5QDB0yHhGhuzOfDlnyHN6sflL4El/MpUP1y8uY2lUZrv4oBS6pTT3g==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.2.tgz", - "integrity": "sha512-hKgB3nz/TKD3Wv78XEsyXzQsNjvhOHmwKQTvXADGOyU/cIClZDO7DsoggbdmJDPGp5V80tA3Vfv61PaKTLH3LA==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.2.tgz", - "integrity": "sha512-T4wf1mudIDxN8Q/CWIBJC1u5gQUc+r5mPvlwoSbIvNkyVTP2TAFeobEmst5AQ4gMyAz4sSByVdoTDfvTmGK/8g==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.2.tgz", - "integrity": "sha512-tC3IY7qoaD9Ll3/8WJQn49j5V2f/NuI9S41NOE2iM5MPs3sPIvOkVToLcz/7Bz4pyF7PSvrtwu8I/pUrGOSecQ==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.2.tgz", - "integrity": "sha512-6NHnk/K3eq2ZFYcU1X8g67s9qIJRCOTT92gwLMVBp08dB2uuuwI1/Q/empzL2Bfr2f2WRLJVwpp90RmacQyFkw==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", - "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.63.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.2.tgz", - "integrity": "sha512-l5eyksV4tPBj6lJyEa37YzIOCSOV7lkZzEHUdpjWZbtD7wTcFYmEYXSgm5bT4vV+dZLb9rBG1W9GROOG4NS4Ew==", + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.2", - "@rollup/rollup-android-arm64": "4.63.2", - "@rollup/rollup-darwin-arm64": "4.63.2", - "@rollup/rollup-darwin-x64": "4.63.2", - "@rollup/rollup-freebsd-arm64": "4.63.2", - "@rollup/rollup-freebsd-x64": "4.63.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.2", - "@rollup/rollup-linux-arm-musleabihf": "4.63.2", - "@rollup/rollup-linux-arm64-gnu": "4.63.2", - "@rollup/rollup-linux-arm64-musl": "4.63.2", - "@rollup/rollup-linux-loong64-gnu": "4.63.2", - "@rollup/rollup-linux-loong64-musl": "4.63.2", - "@rollup/rollup-linux-ppc64-gnu": "4.63.2", - "@rollup/rollup-linux-ppc64-musl": "4.63.2", - "@rollup/rollup-linux-riscv64-gnu": "4.63.2", - "@rollup/rollup-linux-riscv64-musl": "4.63.2", - "@rollup/rollup-linux-s390x-gnu": "4.63.2", - "@rollup/rollup-linux-x64-gnu": "4.63.2", - "@rollup/rollup-linux-x64-musl": "4.63.2", - "@rollup/rollup-openbsd-x64": "4.63.2", - "@rollup/rollup-openharmony-arm64": "4.63.2", - "@rollup/rollup-win32-arm64-msvc": "4.63.2", - "@rollup/rollup-win32-ia32-msvc": "4.63.2", - "@rollup/rollup-win32-x64-gnu": "4.63.2", - "@rollup/rollup-win32-x64-msvc": "4.63.2", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 18dcd7ea05..819149a3bf 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.5.1", + "@types/node": "^26.6.2", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.2", + "rollup": "^4.63.4", "terser": "^5.51.2", "typescript": "^7.0.2" }, From d1822b44b968607d94ac2696b0c98ef81bd11c0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 18:24:09 +0200 Subject: [PATCH 087/322] chore: Bump the all-nuget group with 3 updates (#4990) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Fable.Build/Fable.Build.fsproj | 2 +- tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index bfda1de7f4..1df8f9115e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.0", + "version": "8.0.2", "commands": [ "fantomas" ], diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index 469bde2bac..944c97afe6 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -46,7 +46,7 @@ - + diff --git a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj index c1a6a7e835..7c31f7d3ed 100644 --- a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj +++ b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj @@ -10,7 +10,7 @@ - + From 70278ec636b7f948a6ba3d215b36a3974de89edc Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 23 Sep 2026 23:20:57 +0200 Subject: [PATCH 088/322] fix(beam): preserve local spawn calls (#4992) --- src/Fable.Transforms/Beam/ErlangPrinter.fs | 3 +-- tests/Beam/NonRegressionTests.fs | 29 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Beam/ErlangPrinter.fs b/src/Fable.Transforms/Beam/ErlangPrinter.fs index fc2192ddce..3be132da7b 100644 --- a/src/Fable.Transforms/Beam/ErlangPrinter.fs +++ b/src/Fable.Transforms/Beam/ErlangPrinter.fs @@ -206,8 +206,7 @@ module Output = | "throw" | "exit" | "self" - | "node" - | "spawn" -> $"erlang:%s{func}" + | "node" -> $"erlang:%s{func}" | _ -> func sb.Append($"%s{qualifiedFunc}(") |> ignore diff --git a/tests/Beam/NonRegressionTests.fs b/tests/Beam/NonRegressionTests.fs index db7275e807..ab410f7209 100644 --- a/tests/Beam/NonRegressionTests.fs +++ b/tests/Beam/NonRegressionTests.fs @@ -164,3 +164,32 @@ module IssueInlineMutualRec = [] let ``test inline outer with nested mutual recursion captures outer scope`` () = IssueInlineMutualRec.outer 0 |> equal "ok" + +let spawn first second = first, second +let start first second = spawn first second + +[] +let erlangSpawn (moduleName: obj) (functionName: obj) (args: obj) : obj = nativeOnly + +let sendSpawned parent = Fable.Core.BeamInterop.emitErlExpr parent "$0 ! spawned" + +[] +let ``test local spawn function is not replaced by Erlang BIF`` () = + start 1 2 |> equal (1, 2) + +[] +let ``test explicitly imported Erlang spawn BIF stays qualified`` () = +#if FABLE_COMPILER + let parent: obj = Fable.Core.BeamInterop.emitErlExpr () "erlang:self()" + let moduleName: obj = Fable.Core.BeamInterop.emitErlExpr () "?MODULE" + let functionName: obj = Fable.Core.BeamInterop.emitErlExpr () "send_spawned" + let args: obj = Fable.Core.BeamInterop.emitErlExpr parent "[$0]" + erlangSpawn moduleName functionName args |> ignore + + let received: bool = + Fable.Core.BeamInterop.emitErlExpr () "receive spawned -> true after 1000 -> false end" + + received |> equal true +#else + () +#endif From b0c6129e146318701bd723488af6cf8e3d4bdf9b Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 24 Sep 2026 21:19:30 +0200 Subject: [PATCH 089/322] fix: retry MSBuild project cracking serially (#4991) --- src/Fable.Compiler/MSBuildCrackerResolver.fs | 51 +++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index ee81a3e976..53c16f9dda 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -7,6 +7,7 @@ open System.Diagnostics open System.Text.Json open Fable open Fable.AST +open Fable.Compiler.Util open Fable.Compiler.ProjectCracker module private MSBuildCrackerResolver = @@ -28,13 +29,7 @@ module private MSBuildCrackerResolver = type FullPath = string - let private dotnet_msbuild_with_defines - (cwd: string) - (fsproj: FullPath) - (args: string) - (defines: string list) - : Async - = + let private run_dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) (defines: string list) = backgroundTask { let psi = ProcessStartInfo "dotnet" @@ -64,15 +59,45 @@ module private MSBuildCrackerResolver = let error = ps.StandardError.ReadToEnd() do! ps.WaitForExitAsync() - let fullCommand = $"dotnet %s{psi.Arguments}" - - if ps.ExitCode <> 0 then - failwithf $"In %s{psi.WorkingDirectory}:\n%s{fullCommand}\nfailed with\n%s{error}" - - return output.Trim() + return + {| + ExitCode = ps.ExitCode + Output = output.Trim() + Error = error.Trim() + Command = $"dotnet %s{psi.Arguments}" + |} } |> Async.AwaitTask + let private dotnet_msbuild_with_defines cwd fsproj args defines = + // MSBuild can fail without diagnostics in some environments when building project references in parallel, + // so retry this opaque failure once with BuildInParallel disabled. + let rec run retrySerially args = + async { + let! result = run_dotnet_msbuild cwd fsproj args defines + + if result.ExitCode = 0 then + return result.Output + elif + retrySerially + && String.IsNullOrWhiteSpace result.Output + && not (result.Error.Contains(": error ", StringComparison.OrdinalIgnoreCase)) + && result.Error.Contains( + "Build failed. Properties, Items, and Target results cannot be obtained.", + StringComparison.Ordinal + ) + then + Log.warning "MSBuild project evaluation failed without diagnostics. Retrying serially." + return! run false $"%s{args} /p:BuildInParallel=false" + else + return + $"In %s{cwd}:\n%s{result.Command}\nfailed with exit code %i{result.ExitCode}\n%s{result.Error}" + |> Fable.FableError + |> raise + } + + run true args + let private dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) : Async = dotnet_msbuild_with_defines cwd fsproj args List.empty From ece7b0714eae18650124cad017b0bda4da74ebc8 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 25 Sep 2026 10:59:10 +0200 Subject: [PATCH 090/322] fix(dart): prefix file scope names shadowed by class members (#4982) --- src/Fable.Transforms/Dart/Fable2Dart.fs | 49 ++++++++++++++++++++++++- tests/Dart/src/RecordTests.fs | 26 +++++++++++++ tests/Dart/src/TypeTests.fs | 18 +++++++++ tests/Dart/src/UnionTests.fs | 11 ++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 8f26f510b8..5f52032d7a 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -44,6 +44,7 @@ type Context = /// Vars declared in current function scope VarsDeclaredInScope: HashSet ConstIdents: Set + ShadowingMemberNames: Set } member this.AddToScope(name) = @@ -291,6 +292,44 @@ module Util = | _ -> "_") name + /// In Dart, class members shadow the file scope within the class body, see https://github.com/fable-compiler/Fable/issues/4979 + let isShadowedByClassMember (ctx: Context) (name: string) = + let isLocalName = + let locals = ctx.UsedNames.CurrentDeclarationScope + not (isNull locals) && locals.Contains(name) + + ctx.ShadowingMemberNames.Contains(name) + && ctx.File.UsedNamesInRootScope.Contains(name) + && not isLocalName + + let getShadowingMemberNames (ent: Fable.Entity) (decl: Fable.ClassDecl) = + let dispatchSlots = + ent.MembersFunctionsAndValues + |> Seq.choose (fun memb -> + if memb.IsDispatchSlot then + Some memb.DisplayName + else + None + ) + + set + [ + yield! dispatchSlots + yield! ent.FSharpFields |> List.map (fun f -> sanitizeMember f.Name) + yield! decl.AttachedMembers |> List.map (fun memb -> sanitizeMember memb.Name) + if ent.IsFSharpUnion then + "tag" + + yield! + ent.UnionCases + |> List.collect (fun uci -> uci.UnionCaseFields |> List.map (fun f -> sanitizeMember f.Name)) + ] + + /// A Dart library can import itself + let getSelfImportPrefix (com: IDartCompiler) ctx = + let selfModule = com.GetImportIdent(ctx, "*", com.CurrentFile, Fable.MetaType) + selfModule.Name + let getUniqueNameInRootScope (ctx: Context) name = let name = (name, Naming.NoMemberPart) @@ -704,7 +743,12 @@ module Util = makeIdent isMutable typ name let transformIdent (com: IDartCompiler) ctx (id: Fable.Ident) : Ident = - transformIdentWith com ctx id.IsMutable id.Type id.Name + let ident = transformIdentWith com ctx id.IsMutable id.Type id.Name + + if isShadowedByClassMember ctx id.Name then + { ident with ImportModule = Some(getSelfImportPrefix com ctx) } + else + ident let transformIdentAsExpr (com: IDartCompiler) ctx (id: Fable.Ident) = transformIdent com ctx id |> Expression.identExpression @@ -3035,6 +3079,8 @@ module Util = let entRef = decl.Entity let ent = com.GetEntity(entRef) + let ctx = { ctx with ShadowingMemberNames = getShadowingMemberNames ent decl } + if ent.IsInterface then transformInterfaceDeclaration com ctx decl ent else @@ -3195,6 +3241,7 @@ module Compiler = OptimizeTailCall = fun () -> () VarsDeclaredInScope = HashSet() ConstIdents = Set.empty + ShadowingMemberNames = Set.empty } let rootDecls = List.collect (transformDeclaration com ctx) file.Declarations diff --git a/tests/Dart/src/RecordTests.fs b/tests/Dart/src/RecordTests.fs index 179801b326..492b44ec17 100644 --- a/tests/Dart/src/RecordTests.fs +++ b/tests/Dart/src/RecordTests.fs @@ -42,6 +42,22 @@ type Time = type CarInterior = { Seats: int } type Car = { Interior: CarInterior } +type RecordWithIdField = { Id: Id } + +type Wrapped = { Value: int } + +let addOne (x: int) = x + 1 + +type IWrap = + abstract Wrap: unit -> Wrapped + abstract Add: unit -> int + +type RecordShadowingFileScope = + { Wrapped: int; addOne: int } + interface IWrap with + member this.Wrap() = { Value = this.Wrapped } + member this.Add() = addOne this.addOne + let tests() = testCase "Anonymous records work" <| fun () -> let r = makeAnonRec() @@ -155,3 +171,13 @@ let tests() = let car2 = {| car with Interior.Seats = 5 |} equal 5 car2.Interior.Seats + + testCase "Record field can have the same name as its type" <| fun () -> + let record = { Id = Id "foo" } + match record.Id with + | Id value -> value |> equal "foo" + + testCase "Record fields can have the same name as file scope declarations" <| fun () -> + let record = { Wrapped = 3; addOne = 7 } :> IWrap + record.Wrap().Value |> equal 3 + record.Add() |> equal 8 diff --git a/tests/Dart/src/TypeTests.fs b/tests/Dart/src/TypeTests.fs index 5a498c02f4..fb6e41d578 100644 --- a/tests/Dart/src/TypeTests.fs +++ b/tests/Dart/src/TypeTests.fs @@ -595,6 +595,17 @@ type Model = unit let update (model: Model) = model, () +type Id = Id of string + +type IHasId = + abstract Id: Id + abstract GetId: unit -> Id + +type IdHolder(id: Id) = + interface IHasId with + member _.Id = id + member _.GetId() = id + let tests() = testCase "Unit arguments work" <| fun () -> @@ -614,6 +625,13 @@ let tests() = ex.Message |> equal "outer message" ex.InnerException.Message |> equal "the inner cause" + testCase "Interface members can have the same name as their type" <| fun () -> + let holder = IdHolder(Id "foo") :> IHasId + match holder.Id with + | Id value -> value |> equal "foo" + match holder.GetId() with + | Id value -> value |> equal "foo" + // testCase "Can implement interface optional properties" <| fun () -> // let veryOptionalValue = VeryOptionalClass() :> VeryOptionalInterface // veryOptionalValue.Bar |> equal (Some 3) diff --git a/tests/Dart/src/UnionTests.fs b/tests/Dart/src/UnionTests.fs index ef89a7f466..11125bea46 100644 --- a/tests/Dart/src/UnionTests.fs +++ b/tests/Dart/src/UnionTests.fs @@ -89,6 +89,10 @@ type T8 = T8 type T9 = T9 type 'a more = More of 'a +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + let tests() = testCase "Union cases matches with no arguments can be generated" <| fun () -> let x = Male @@ -200,6 +204,13 @@ let tests() = |> Array.length |> equal 2 + testCase "Union field can have the same name as its type" <| fun () -> + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + // TODO // testCase "Case testing with erased unions works" <| fun () -> // let strify = function From 32d85add01422547dba03b15e0a0787d8afb031b Mon Sep 17 00:00:00 2001 From: Goswin Rothenthal Date: Thu, 24 Sep 2026 14:58:26 +0200 Subject: [PATCH 091/322] perf(js/ts): faster `resizeArray.Clear()` in JS/TS --- src/fable-library-ts/CollectionUtil.ts | 22 ++++++++-------------- src/fable-library-ts/Util.ts | 12 ++++++++---- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/fable-library-ts/CollectionUtil.ts b/src/fable-library-ts/CollectionUtil.ts index b3df1fbd5f..7fda114eb7 100644 --- a/src/fable-library-ts/CollectionUtil.ts +++ b/src/fable-library-ts/CollectionUtil.ts @@ -143,19 +143,13 @@ export function remove(col: Iterable, item: T): boolean { export function clear(col: Iterable): void { if (typeof (col as any)["System.Collections.Generic.ICollection`1.Clear"] === "function") { return (col as any)["System.Collections.Generic.ICollection`1.Clear"](); // collection + } else if (Array.isArray(col)) { + col.length = 0; + } else if (ArrayBuffer.isView(col)) { + // TODO: throw for typed arrays? + } else if (typeof (col as any).clear === "function") { + (col as any).clear(); // map, set } else { - if (isArrayLike(col)) { - if (ArrayBuffer.isView(col)) { - // TODO: throw for typed arrays? - } else { - col.splice(0); // array, resize array - } - } else { - if (typeof (col as any).clear === "function") { - (col as any).clear(); // map, set - } else { - // TODO: throw for other collections? - } - } + // TODO: throw for other collections? } -} \ No newline at end of file +} diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 9961578a69..838ee7e74f 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -709,11 +709,15 @@ export function count(col: Iterable): number { } } -export function clear(col: Iterable) { - if (isArrayLike(col)) { - (col as any as T[]).splice(0); +export function clear(col: Iterable): void { + if (Array.isArray(col)) { + col.length = 0; + } else if (ArrayBuffer.isView(col)) { + // TODO: throw for typed arrays? + } else if (typeof (col as any).clear === "function") { + (col as any).clear(); // map, set } else { - (col as any).clear(); + // TODO: throw for other collections? } } From 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Fri, 25 Sep 2026 16:38:31 +0200 Subject: [PATCH 092/322] fix(js/ts): clear custom System.Collections.IList implementations --- src/fable-library-ts/Util.ts | 4 ++++ tests/Js/Main/ResizeArrayTests.fs | 35 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 838ee7e74f..00ad548e9a 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -716,6 +716,10 @@ export function clear(col: Iterable): void { // TODO: throw for typed arrays? } else if (typeof (col as any).clear === "function") { (col as any).clear(); // map, set + } else if (typeof (col as any)["System.Collections.Generic.ICollection`1.Clear"] === "function") { + (col as any)["System.Collections.Generic.ICollection`1.Clear"](); // collection + } else if (typeof (col as any)["System.Collections.IList.Clear"] === "function") { + (col as any)["System.Collections.IList.Clear"](); // collection } else { // TODO: throw for other collections? } diff --git a/tests/Js/Main/ResizeArrayTests.fs b/tests/Js/Main/ResizeArrayTests.fs index 85a9b0d44e..fe0b8c22d6 100644 --- a/tests/Js/Main/ResizeArrayTests.fs +++ b/tests/Js/Main/ResizeArrayTests.fs @@ -6,6 +6,34 @@ open Fable.Tests.Util type Animal = Duck of int | Dog of int +// Fable routes System.Collections.IList members to the ResizeArray replacements +type CustomList() = + let items = ResizeArray() + member _.Items = items + interface System.Collections.IList with + member _.Item + with get i = items.[i] + and set i v = items.[i] <- v + member _.IndexOf(item) = items.IndexOf(item) + member _.Insert(i, item) = items.Insert(i, item) + member _.RemoveAt(i) = items.RemoveAt(i) + member _.Count = items.Count + member _.IsReadOnly = false + member _.IsFixedSize = false + member _.IsSynchronized = false + member _.SyncRoot = box items + member _.Add(item) = + items.Add(item) + items.Count - 1 + member _.Clear() = items.Clear() + member _.Contains(item) = items.Contains(item) + member _.CopyTo(array, arrayIndex) = items.CopyTo(array :?> obj[], arrayIndex) + member _.Remove(item) = items.Remove(item) |> ignore + interface IEnumerable with + member _.GetEnumerator() = (items :> IEnumerable).GetEnumerator() + interface System.Collections.IEnumerable with + member _.GetEnumerator() = (items :> System.Collections.IEnumerable).GetEnumerator() + let tests = testList "ResizeArrays" [ testCase "ResizeArray zero creation works" <| fun () -> @@ -350,6 +378,13 @@ let tests = coll.Clear() coll.Count |> equal 0 + testCase "IList.Clear works on a custom collection" <| fun _ -> + let xs = CustomList() + xs.Items.Add(box 1) + xs.Items.Add(box 2) + (xs :> System.Collections.IList).Clear() + xs.Items.Count |> equal 0 + testCase "ResizeArray ICollection.Add works" <| fun _ -> let xs = [| ("A", 1); ("B", 2); ("C", 3) |] let coll = (ResizeArray xs) :> ICollection<_> From 6ccad2d4f874427a0743f432561e5b67a14c2ae8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:05:52 +0200 Subject: [PATCH 093/322] chore: release multiple projects (#4971) --- src/Fable.Cli/CHANGELOG.md | 25 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 25 ++++++++++++++++++++++++- src/Fable.Core/CHANGELOG.md | 10 +++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 18 +++++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 15 ++++++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 24 +++++++++++++++++++++++- src/fable-standalone/package.json | 4 ++-- 10 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 7bae9f33e9..aa9ea9c7ca 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 66efd10960c967660084db2b364e944d167897f4 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.18.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* Retry MSBuild project cracking serially (#4991) ([b0c6129e](https://github.com/fable-compiler/Fable/commit/b0c6129e146318701bd723488af6cf8e3d4bdf9b)) +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/66efd10960c967660084db2b364e944d167897f4..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 5.17.2 - 2026-09-12 ### 🐞 Bug Fixes diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 52c450c12e..052f94584d 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 66efd10960c967660084db2b364e944d167897f4 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.17.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* Retry MSBuild project cracking serially (#4991) ([b0c6129e](https://github.com/fable-compiler/Fable/commit/b0c6129e146318701bd723488af6cf8e3d4bdf9b)) +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/66efd10960c967660084db2b364e944d167897f4..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 5.16.2 - 2026-09-12 ### 🐞 Bug Fixes diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index 9b498af19a..089f37dd0d 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c977d78b39225a51c7bd051a1fe363ed0ccbe201 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.3.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c977d78b39225a51c7bd051a1fe363ed0ccbe201..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 5.2.0 - 2026-06-30 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 806a2d2a10..7b05d173c2 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.17.2" + let VERSION = "5.18.0" [] - let JS_LIBRARY_VERSION = "2.7.1" + let JS_LIBRARY_VERSION = "2.8.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index c84662181c..e3b2d7f139 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.5.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 3.4.0 - 2026-09-11 ### 🐞 Bug Fixes diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 5d818b15dd..178671e52f 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.4.0", + "version": "3.5.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.4.0" + "@fable-org/fable-standalone": "^3.5.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index e083a94ebf..eef1a37bc5 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 updaters: - package.json: file: package.json @@ -15,6 +15,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.8.0 - 2026-09-25 + +### 🐞 Bug Fixes + +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 2.7.1 - 2026-09-11 ### 🐞 Bug Fixes diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index 4145cefb10..bbc2f13cea 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.7.1", + "version": "2.8.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 5d5289deda..7ae000f9f8 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 4a441b1926387a42db6a3a4294acd97b02760ca3 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.5.0 - 2026-09-25 + +### 🚀 Features + +* *(dart)* Add `Async` runtime and `Future` interop for dart (#4978) ([b5a337d0](https://github.com/fable-compiler/Fable/commit/b5a337d0f71b2b1ae91516b8be8a65c7cadc9953)) + +### 🐞 Bug Fixes + +* *(beam)* Preserve local spawn calls (#4992) ([70278ec6](https://github.com/fable-compiler/Fable/commit/70278ec636b7f948a6ba3d215b36a3974de89edc)) +* *(dart)* Prefix file scope names shadowed by class members (#4982) ([ece7b071](https://github.com/fable-compiler/Fable/commit/ece7b0714eae18650124cad017b0bda4da74ebc8)) +* *(js/ts)* Fix Dictionary.Remove and HashSet.Remove to remove empty buckets (#4985) ([aea61b56](https://github.com/fable-compiler/Fable/commit/aea61b562c461a9b383e805f06f68a3b21ed45c2)) +* *(js/ts)* Clear custom System.Collections.IList implementations ([2369fbd2](https://github.com/fable-compiler/Fable/commit/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36)) +* *(python)* Keep the payload type when reading an erased union field (#4981) ([e0536ecd](https://github.com/fable-compiler/Fable/commit/e0536ecd9b9aeb12cc96c7a2ef2edde7089e2dd0)) +* *(rust)* Less allocations when enumerating strings (#4970) ([5d62380c](https://github.com/fable-compiler/Fable/commit/5d62380cb30dc1080c6943445a256e27db6246f8)) +* *(rust)* Remove reflection registration on boxing (#4983) ([50a37f78](https://github.com/fable-compiler/Fable/commit/50a37f78f31394d61349b09a025c1713529d8ec0)) + +### ⚡ Performance Improvements + +* *(js/ts)* Faster `resizeArray.Clear()` in JS/TS ([32d85add](https://github.com/fable-compiler/Fable/commit/32d85add01422547dba03b15e0a0787d8afb031b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/4a441b1926387a42db6a3a4294acd97b02760ca3..2369fbd24fafe851544c1ed6eb54f0bfc2b75f36) + ## 3.4.0 - 2026-09-11 ### 🐞 Bug Fixes diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 9ba4d8e3c0..8cf452f77e 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.4.0", - "fableVersion": "5.17.2", + "version": "3.5.0", + "fableVersion": "5.18.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From ae165dd0cf7248e22c3520d1e00347d0713d0526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:59:51 -0700 Subject: [PATCH 094/322] fix(rust): Use common null storage (#4995) --- src/fable-library-rust/src/Native.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index 32068de059..105b1cc0ab 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -101,6 +101,8 @@ pub mod Native_ { // impl Downcast for T {} + static NULL: OnceInit> = OnceInit::new(); + pub trait NullableRef { fn null() -> Self; fn is_null(&self) -> bool; @@ -123,7 +125,6 @@ pub mod Native_ { impl NullableRef for Lrc { #[inline] fn null() -> Self { - static NULL: OnceInit> = OnceInit::new(); NULL.get_or_init(|| Lrc::new(())).clone() } @@ -315,8 +316,7 @@ pub mod Native_ { } pub fn null_ptr() -> Lrc { - static NULL: OnceInit> = OnceInit::new(); - let null_rc = NULL.get_or_init(move || Lrc::new(())); + let null_rc = NULL.get_or_init(|| Lrc::new(())); unsafe { Lrc::from_raw(Lrc::into_raw(null_rc.clone()) as *const T) } } From 389edefb5ed131ed14fea26a9aa04dde4f5c40cf Mon Sep 17 00:00:00 2001 From: Leo <45302927+Tseing@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:07:33 +0800 Subject: [PATCH 095/322] feat(dart): add support for `System.Text.Rune` (#3860) (#4969) --- src/Fable.Transforms/Dart/Fable2Dart.fs | 3 +- src/Fable.Transforms/Dart/Replacements.fs | 14 +++ src/fable-library-dart/String.dart | 27 +++++ tests/Dart/main.dart | 2 + tests/Dart/src/Fable.Tests.Dart.fsproj | 1 + tests/Dart/src/RuneTests.fs | 120 ++++++++++++++++++++++ 6 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 tests/Dart/src/RuneTests.fs diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 5f52032d7a..658947e807 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -220,7 +220,8 @@ module Util = Type.reference (makeImmutableIdent MetaType "MapEntry", [ key; value ]) match entRef.FullName, genArgs with - | Types.enum_, _ -> Integer + | Types.enum_, _ + | "System.Text.Rune", _ -> Integer // F# Quotation types are modelled dynamically by the quotation runtime, // so map them to `dynamic` (the runtime returns plain tagged objects). | Types.fsharpExpr, _ diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 7cbf020b62..7bb4101188 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -247,6 +247,7 @@ let toString com (ctx: Context) r (args: Expr list) = match head.Type with | String -> head | Char -> charToString head + | DeclaredType({ FullName = "System.Text.Rune" }, _) -> charToString head // | Builtin BclGuid when tail.IsEmpty -> head // | Builtin (BclGuid|BclTimeSpan|BclTimeOnly|BclDateOnly as bt) -> // Helper.LibCall(com, coreModFor bt, "toString", String, args) @@ -1406,6 +1407,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "compareWith", Int32.Number, [ x; y; kind ]) makeEqOp r left (makeIntConst 0) BinaryEqual |> Some + | "EnumerateRunes", Some c, _ -> Helper.LibCall(com, "String", "enumerateRunes", t, [ c ], ?loc = r) |> Some | "GetEnumerator", Some c, _ -> stringToCharSeq c |> getEnumerator com r t |> Some | ("Contains" | "StartsWith" | "EndsWith" as meth), Some c, arg :: _ -> if List.isMultiple args then @@ -1562,6 +1564,17 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> Some | _ -> None +let runes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + match i.CompiledName, thisArg, args with + | "GetRuneAt", None, [ input; index ] -> + Helper.LibCall(com, "String", "getRuneAt", t, [ input; index ], i.SignatureArgTypes, ?loc = r) + |> Some + | "get_Value", Some rune, [] -> Some rune + | "get_Utf16SequenceLength", Some runes, [] -> + let isBmp = makeEqOp r runes (makeIntConst 0xFFFF) BinaryLessOrEqual + IfThenElse(isBmp, makeIntConst 1, makeIntConst 2, r) |> Some + | _ -> None + let stringModule (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with | "Length", [ arg ] -> getLength arg |> Some @@ -3952,6 +3965,7 @@ let private replacedModules = "System.Runtime.ExceptionServices.ExceptionDispatchInfo", exceptionDispatchInfo Types.char, chars Types.string, strings + "System.Text.Rune", runes "Microsoft.FSharp.Core.StringModule", stringModule "System.FormattableString", formattableString "System.Runtime.CompilerServices.FormattableStringFactory", formattableString diff --git a/src/fable-library-dart/String.dart b/src/fable-library-dart/String.dart index e3d1a7d212..33b593c3c6 100644 --- a/src/fable-library-dart/String.dart +++ b/src/fable-library-dart/String.dart @@ -87,6 +87,33 @@ int getCharAtIndex(String input, int index) { return input.codeUnitAt(index); } +int getRuneAt(String input, int index) { + if (index < 0 || index >= input.length) { + throw Exception('Index was outside the bounds of the array.'); + } + + late final iterator; + try { + iterator = RuneIterator.at(input, index); + } on ArgumentError catch (e) { + throw Exception(e.message); + } + + iterator.moveNext(); + final rune = iterator.current; + + if (rune >= 0xD800 && rune <= 0xDFFF) { + throw Exception('Invalid UTF-16 sequence at index $index.'); + } + return rune; +} + +Iterable enumerateRunes(String input) { + return input.runes.map( + (rune) => rune >= 0xD800 && rune <= 0xDFFF ? 0xFFFD : rune, + ); +} + List splitWithChars(String str, [List? splitters, int? count, int? options]) { splitters ??= []; diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart index a177d68b7f..9149d8cba7 100644 --- a/tests/Dart/main.dart +++ b/tests/Dart/main.dart @@ -20,6 +20,7 @@ import './src/RecordTests.dart' as record; import './src/RegexTests.dart' as regex; import './src/ResizeArrayTests.dart' as resize_array; import './src/ResultTests.dart' as result; +import './src/RuneTests.dart' as rune; import './src/SeqTests.dart' as seq; import './src/SeqExpressionTests.dart' as seq_expression; import './src/SetTests.dart' as set_; @@ -54,6 +55,7 @@ void main() { regex.tests(); resize_array.tests(); result.tests(); + rune.tests(); seq.tests(); seq_expression.tests(); set_.tests(); diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index 55750b54b8..4cbb0780d8 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -30,6 +30,7 @@ + diff --git a/tests/Dart/src/RuneTests.fs b/tests/Dart/src/RuneTests.fs new file mode 100644 index 0000000000..9a307aebdc --- /dev/null +++ b/tests/Dart/src/RuneTests.fs @@ -0,0 +1,120 @@ +module Fable.Tests.Dart.Rune + +open System.Text +open Fable.Core +open Util + +[] +let private stringFromCharCode (_: int) : string = + nativeOnly + +let tests () = + testCase "Rune.GetRuneAt works with BMP characters" <| fun () -> + let rune = Rune.GetRuneAt("ABC", 1) + + rune.Value |> equal 0x42 + rune.ToString() |> equal "B" + + testCase "Rune.GetRuneAt works with supplementary characters" <| fun () -> + let rune = Rune.GetRuneAt("𠀀", 0) + + rune.Value |> equal 0x20000 + rune.ToString() |> equal "𠀀" + + testCase "Rune.GetRuneAt uses UTF-16 code unit indexes" <| fun () -> + Rune.GetRuneAt("A𠀀B", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("A𠀀B", 1).ToString() + |> equal "𠀀" + + Rune.GetRuneAt("A𠀀B", 3).ToString() + |> equal "B" + + testCase "Rune.GetRuneAt throws when index points inside a surrogate pair" <| fun () -> + throwsAnyError <| fun () -> + Rune.GetRuneAt("A𠀀B", 2) + |> ignore + + testCase "Rune.GetRuneAt throws when index is out of range" <| fun () -> + throwsAnyError <| fun () -> + Rune.GetRuneAt("ABC", -1) + |> ignore + + throwsAnyError <| fun () -> + Rune.GetRuneAt("ABC", 3) + |> ignore + + testCase "Rune.GetRuneAt throws for unpaired surrogate characters" <| fun () -> + let highSurrogate = stringFromCharCode 0xD800 + let lowSurrogate = stringFromCharCode 0xDC00 + + throwsAnyError <| fun () -> + Rune.GetRuneAt(highSurrogate, 0) + |> ignore + + throwsAnyError <| fun () -> + Rune.GetRuneAt(lowSurrogate, 0) + |> ignore + + testCase "Rune.Value returns the Unicode scalar value" <| fun () -> + Rune.GetRuneAt("A", 0).Value + |> equal 0x41 + + Rune.GetRuneAt("𠀀", 0).Value + |> equal 0x20000 + + testCase "Rune.Utf16SequenceLength returns the number of UTF-16 code units" <| fun () -> + Rune.GetRuneAt("A", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("𠀀", 0).Utf16SequenceLength + |> equal 2 + + testCase "Rune.Utf16SequenceLength works at the BMP boundary" <| fun () -> + Rune.GetRuneAt("\uFFFF", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("\U00010000", 0).Utf16SequenceLength + |> equal 2 + + testCase "Rune.ToString returns the represented Unicode scalar as a string" <| fun () -> + Rune.GetRuneAt("A", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("𠀀", 0).ToString() + |> equal "𠀀" + + testCase "F# string conversion works with Rune" <| fun () -> + Rune.GetRuneAt("A", 0) + |> string + |> equal "A" + + Rune.GetRuneAt("𠀀", 0) + |> string + |> equal "𠀀" + + testCase "String.EnumerateRunes enumerates Unicode scalar values" <| fun () -> + "A𠀀B".EnumerateRunes() + |> Seq.map (fun rune -> rune.Value) + |> Seq.toList + |> equal [ 0x41; 0x20000; 0x42 ] + + testCase "String.EnumerateRunes works with Rune string conversion" <| fun () -> + "A𠀀B".EnumerateRunes() + |> Seq.map string + |> Seq.toList + |> equal [ "A"; "𠀀"; "B" ] + + testCase "String.EnumerateRunes replaces unpaired surrogates" <| fun () -> + // .NET String.EnumerateRunes replaces invalid UTF-16 sequences with U+FFFD + // https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.replacementchar + let input = + stringFromCharCode 0xD800 + + "A" + + stringFromCharCode 0xDC00 + + input.EnumerateRunes() + |> Seq.map _.Value + |> Seq.toList + |> equal [ 0xFFFD; 0x41; 0xFFFD ] From bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:51:33 -0700 Subject: [PATCH 096/322] fix(rust): Add support for System.Text.Rune (#4997) --- src/Fable.Transforms/Replacements.Util.fs | 2 + .../Rust/AST/Rust.AST.Helpers.fs | 40 ++ src/Fable.Transforms/Rust/Fable2Rust.fs | 3 + src/Fable.Transforms/Rust/Replacements.fs | 76 +++- src/Fable.Transforms/Transforms.Util.fs | 3 + src/fable-library-rust/src/Rune.rs | 283 +++++++++++++ src/fable-library-rust/src/String.rs | 4 + src/fable-library-rust/src/lib.fs | 1 + tests/Rust/Fable.Tests.Rust.fsproj | 1 + tests/Rust/tests/src/RuneTests.fs | 371 ++++++++++++++++++ 10 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 src/fable-library-rust/src/Rune.rs create mode 100644 tests/Rust/tests/src/RuneTests.fs diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index f97e14f0b5..fe3b8ddf50 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -294,6 +294,7 @@ type BuiltinType = | BclDateTimeOffset | BclDateOnly | BclTimeOnly + | BclRune | BclTimer | BclHashSet of Type | BclDictionary of key: Type * value: Type @@ -313,6 +314,7 @@ let (|BuiltinDefinition|_|) = | Types.datetimeOffset -> ValueSome BclDateTimeOffset | Types.dateOnly -> ValueSome BclDateOnly | Types.timeOnly -> ValueSome BclTimeOnly + | Types.rune -> ValueSome BclRune | "System.Timers.Timer" -> ValueSome BclTimer | Types.fsharpSet -> ValueSome(FSharpSet(Any)) | Types.fsharpMap -> ValueSome(FSharpMap(Any, Any)) diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs index f2eecc6918..cd7f4e8550 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs @@ -337,7 +337,47 @@ module Literals = span = DUMMY_SP } + let private normalizeStringLiteral (value: Symbol) = + let mutable builder: ValueOption = ValueNone + let mutable index = 0 + + while index < value.Length do + let current = value[index] + + if + System.Char.IsHighSurrogate(current) + && index + 1 < value.Length + && System.Char.IsLowSurrogate(value[index + 1]) + then + match builder with + | ValueSome builder -> builder.Append(value, index, 2) |> ignore + | ValueNone -> () + + index <- index + 2 + elif System.Char.IsSurrogate(current) then + match builder with + | ValueSome builder -> builder.Append('\uFFFD') |> ignore + | ValueNone -> + let newBuilder = System.Text.StringBuilder(value.Length) + newBuilder.Append(value, 0, index) |> ignore + newBuilder.Append('\uFFFD') |> ignore + builder <- ValueSome newBuilder + + index <- index + 1 + else + match builder with + | ValueSome builder -> builder.Append(current) |> ignore + | ValueNone -> () + + index <- index + 1 + + match builder with + | ValueSome builder -> builder.ToString() + | ValueNone -> value + let mkStrLit (value: Symbol) : Lit = + let value = normalizeStringLiteral value + { token = mkStrTokenLit (value.escape_debug ()) kind = LitKind.Str(value, StrStyle.Cooked) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 0dd6a3533e..9d9f4ae429 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -866,6 +866,8 @@ module TypeInfo = let transformTimeOnlyType com ctx : Rust.Ty = transformImportType com ctx [] "TimeOnly" "TimeOnly" + let transformRuneType _com _ctx : Rust.Ty = primitiveType "char" + let transformTimerType com ctx : Rust.Ty = transformImportType com ctx [] "DateTime" "Timer" @@ -1130,6 +1132,7 @@ module TypeInfo = | Replacements.Util.BclDateTimeOffset -> transformDateTimeOffsetType com ctx | Replacements.Util.BclDateOnly -> transformDateOnlyType com ctx | Replacements.Util.BclTimeOnly -> transformTimeOnlyType com ctx + | Replacements.Util.BclRune -> transformRuneType com ctx | Replacements.Util.BclTimer -> transformTimerType com ctx | Replacements.Util.BclHashSet(genArg) -> transformHashSetType com ctx genArg | Replacements.Util.BclDictionary(k, v) -> transformHashMapType com ctx [ k; v ] diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 2181acbdc5..9ccf6bc93e 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -53,6 +53,7 @@ let coreModFor = | BclDateTimeOffset -> "DateTimeOffset" | BclDateOnly -> "DateOnly" | BclTimeOnly -> "TimeOnly" + | BclRune -> "Rune" | BclTimer -> "Timer" | BclTimeSpan -> "TimeSpan" | FSharpSet _ -> "Set" @@ -427,7 +428,8 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = match argTypes with // | Number(BigInt as kind,_)::_ -> // Helper.LibCall(com, "BigInt", opName, t, args, argTypes, ?loc=r) - | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan) :: _ -> nativeOp opName argTypes args + | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan | BclRune) :: _ -> + nativeOp opName argTypes args | Builtin(FSharpSet _) :: _ -> let methName = match opName with @@ -453,6 +455,7 @@ let isCompatibleWithNativeComparison = // | List _ | Builtin(BclGuid) -> true | Builtin(BclTimeSpan) -> true + | Builtin(BclRune) -> true | _ -> false // Overview of hash rules: @@ -471,6 +474,8 @@ let referenceHash (com: ICompiler) ctx r (arg: Expr) = let getHashCode (com: ICompiler) ctx r (arg: Expr) = match arg.Type with | HasReferenceEquality com _ -> referenceHash com ctx r arg + | Builtin BclRune -> Helper.LibCall(com, "Rune", "getHashCode", Int32.Number, [ arg ], ?loc = r) + | Char -> Helper.LibCall(com, "Char", "GetHashCode", Int32.Number, [ arg ], ?loc = r) | _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) let objectHash (com: ICompiler) ctx r (arg: Expr) = @@ -658,6 +663,7 @@ let rec getZero (com: ICompiler) (ctx: Context) (t: Type) = | Builtin BclDateTimeOffset -> Helper.LibCall(com, "DateTimeOffset", "zero", t, []) | Builtin BclDateOnly -> Helper.LibCall(com, "DateOnly", "zero", t, []) | Builtin BclTimeOnly -> Helper.LibCall(com, "TimeOnly", "zero", t, []) + | Builtin BclRune -> Helper.LibCall(com, "Rune", "zero", t, []) | Builtin BclTimeSpan -> Helper.LibValue(com, "TimeSpan", "zero", t) | Builtin(FSharpSet genArg) -> makeSet com ctx None t [] genArg | Builtin BclGuid -> Helper.LibValue(com, "Guid", "empty", t) @@ -1424,6 +1430,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | _ -> None | "get_Length", Some c, _ -> Helper.LibCall(com, "String", "length", t, c :: args, ?loc = r) |> Some | "get_Chars", Some c, _ -> Helper.LibCall(com, "String", "getCharAt", t, c :: args, ?loc = r) |> Some + | "EnumerateRunes", Some c, _ -> Helper.LibCall(com, "String", "enumerateRunes", t, [ c ], ?loc = r) |> Some | "CompareOrdinal", None, _ -> match args with | [ ExprType String; ExprType String ] -> @@ -2860,6 +2867,69 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | "Add" when args.Length = 2 -> makeInstanceCall r t i thisArg.Value "add2" args |> Some | meth -> makeMemberCall com ctx r t i "TimeOnly" meth thisArg args |> Some +let rune (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let libCall memberName callArgs = + Helper.LibCall(com, "Rune", memberName, t, callArgs, i.SignatureArgTypes, ?loc = r) + + let instanceCall memberName callArgs = + libCall memberName (thisArg.Value :: callArgs) + + let firstArg = + match args with + | arg :: _ -> [ arg ] + | [] -> [] + + let isCultureInfoArg (arg: Expr) = + match arg.Type with + | DeclaredType(ent, _) -> ent.FullName = "System.Globalization.CultureInfo" + | _ -> false + + match i.CompiledName, thisArg, args with + | ".ctor", None, [ ExprType(Char) ] -> Some args.Head + | ".ctor", None, [ ExprType(Number(Int32, _)) ] -> libCall "newInt" args |> Some + | ".ctor", None, [ ExprType(Number(UInt32, _)) ] -> libCall "newUInt" args |> Some + | ".ctor", None, [ ExprType(Char); ExprType(Char) ] -> libCall "newPair" args |> Some + | ("ReplacementChar" | "get_ReplacementChar"), None, [] -> libCall "replacementChar" [] |> Some + | "op_Explicit", None, [ ExprType(Char) ] -> Some args.Head + | "op_Explicit", None, [ ExprType(Number(Int32, _)) ] -> libCall "newInt" args |> Some + | "op_Explicit", None, [ ExprType(Number(UInt32, _)) ] -> libCall "newUInt" args |> Some + | "TryCreate", None, [ ExprType(Char); _ ] -> libCall "tryCreateChar" args |> Some + | "TryCreate", None, [ ExprType(Char); ExprType(Char); _ ] -> libCall "tryCreatePair" args |> Some + | "TryCreate", None, [ ExprType(Number(Int32, _)); _ ] -> libCall "tryCreateInt" args |> Some + | "TryCreate", None, [ ExprType(Number(UInt32, _)); _ ] -> libCall "tryCreateUInt" args |> Some + | "get_Value", Some _, [] -> instanceCall "value" [] |> Some + | "get_Utf8SequenceLength", Some _, [] -> instanceCall "utf8SequenceLength" [] |> Some + | "get_Utf16SequenceLength", Some _, [] -> instanceCall "utf16SequenceLength" [] |> Some + | "get_IsAscii", Some _, [] -> instanceCall "isAscii" [] |> Some + | "get_IsBmp", Some _, [] -> instanceCall "isBmp" [] |> Some + | "get_Plane", Some _, [] -> instanceCall "plane" [] |> Some + | "GetNumericValue", None, _ -> libCall "getNumericValue" firstArg |> Some + | "GetUnicodeCategory", None, _ -> libCall "getUnicodeCategory" firstArg |> Some + | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsUpper" | "IsWhiteSpace"), + None, + _ -> + let meth = Naming.lowerFirst i.CompiledName + libCall meth firstArg |> Some + | "IsValid", None, [ ExprType(Number(Int32, _)) ] -> libCall "isValidInt" args |> Some + | "IsValid", None, [ ExprType(Number(UInt32, _)) ] -> libCall "isValidUInt" args |> Some + | "ToLower", None, [ runeArg; cultureInfo ] when isCultureInfoArg cultureInfo -> + libCall "toLowerInvariant" [ runeArg ] |> Some + | "ToLowerInvariant", None, _ -> libCall "toLowerInvariant" firstArg |> Some + | "ToUpper", None, [ runeArg; cultureInfo ] when isCultureInfoArg cultureInfo -> + libCall "toUpperInvariant" [ runeArg ] |> Some + | "ToUpperInvariant", None, _ -> libCall "toUpperInvariant" firstArg |> Some + | "GetRuneAt", None, _ -> libCall "getRuneAt" args |> Some + | "TryGetRuneAt", None, _ -> libCall "tryGetRuneAt" args |> Some + | "Parse", None, _ -> libCall "parse" firstArg |> Some + | "TryParse", None, _ -> libCall "tryParse" args |> Some + | "ToString", Some _, _ -> instanceCall "toString" [] |> Some + | "CompareTo", Some _, _ -> instanceCall "compareTo" args |> Some + | "Compare", None, _ -> libCall "compareTo" args |> Some + | "Equals", Some _, _ -> instanceCall "equals" args |> Some + | "Equals", None, _ -> libCall "equals" args |> Some + | "GetHashCode", Some _, _ -> libCall "getHashCode" [ thisArg.Value ] |> Some + | _ -> None + let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = // let callee = match i.callee with Some c -> c | None -> i.args.Head match i.CompiledName with @@ -3691,6 +3761,8 @@ let tryField com t ownerTyp fieldName = let meth = fieldName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase Helper.LibValue(com, "TimeSpan", meth, t) |> Some + | Builtin BclRune, "ReplacementChar" + | Builtin BclRune, "get_ReplacementChar" -> Helper.LibValue(com, "Rune", "replacementChar", t) |> Some | Builtin BclDateTime, _ -> let meth = fieldName |> Naming.lowerFirst makeStaticFieldCall com None t "DateTime" "DateTime" meth |> Some @@ -3805,6 +3877,7 @@ let private replacedModules = Types.datetimeOffset, dateTimeOffsets Types.dateOnly, dateOnly Types.timeOnly, timeOnly + Types.rune, rune Types.timespan, timeSpans "System.Timers.Timer", timers "System.Environment", systemEnv @@ -3974,6 +4047,7 @@ let tryType typ = | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffsets, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) + | BclRune -> Some(Types.rune, rune, []) | BclTimer -> Some("System.Timers.Timer", timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 32eec7b374..6621fc3ef5 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -228,6 +228,9 @@ module Types = [] let timeOnly = "System.TimeOnly" + [] + let rune = "System.Text.Rune" + [] let timer = "System.Timers.Timer" diff --git a/src/fable-library-rust/src/Rune.rs b/src/fable-library-rust/src/Rune.rs new file mode 100644 index 0000000000..9cfaf78106 --- /dev/null +++ b/src/fable-library-rust/src/Rune.rs @@ -0,0 +1,283 @@ +pub mod Rune_ { + use crate::Char_::{ + GetNumericValue, GetUnicodeCategory, IsControl, IsDigit, IsLetter, IsLetterOrDigit, + IsLower, IsNumber, IsPunctuation, IsSeparator, IsSymbol, IsUpper, IsWhiteSpace, + ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant, + }; + use crate::Native_::MutCell; + use crate::String_::{ofChar, string}; + + pub fn zero() -> char { + '\u{0000}' + } + + pub fn replacementChar() -> char { + '\u{FFFD}' + } + + fn isValidScalar(value: u32) -> bool { + value <= 0x10FFFF && !(0xD800..=0xDFFF).contains(&value) + } + + fn newScalar(value: u32) -> char { + if isValidScalar(value) { + char::from_u32(value).unwrap() + } else { + panic!("Value is not a valid Unicode scalar value") + } + } + + pub fn newInt(value: i32) -> char { + if value < 0 { + panic!("Value is not a valid Unicode scalar value") + } + newScalar(value as u32) + } + + pub fn newUInt(value: u32) -> char { + newScalar(value) + } + + pub fn newPair(highSurrogate: char, lowSurrogate: char) -> char { + let high = highSurrogate as u32; + let low = lowSurrogate as u32; + if (0xD800..=0xDBFF).contains(&high) && (0xDC00..=0xDFFF).contains(&low) { + let value = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00); + newScalar(value) + } else { + panic!("The values do not form a valid UTF-16 surrogate pair") + } + } + + pub fn tryCreateChar(value: char, result: &MutCell) -> bool { + result.set(value); + true + } + + pub fn tryCreatePair(highSurrogate: char, lowSurrogate: char, result: &MutCell) -> bool { + let high = highSurrogate as u32; + let low = lowSurrogate as u32; + if (0xD800..=0xDBFF).contains(&high) && (0xDC00..=0xDFFF).contains(&low) { + result.set(newScalar(0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00))); + true + } else { + result.set(zero()); + false + } + } + + pub fn tryCreateInt(value: i32, result: &MutCell) -> bool { + if value >= 0 && isValidScalar(value as u32) { + result.set(char::from_u32(value as u32).unwrap()); + true + } else { + result.set(zero()); + false + } + } + + pub fn tryCreateUInt(value: u32, result: &MutCell) -> bool { + if isValidScalar(value) { + result.set(char::from_u32(value).unwrap()); + true + } else { + result.set(zero()); + false + } + } + + pub fn value(value: char) -> i32 { + value as i32 + } + + pub fn utf8SequenceLength(value: char) -> i32 { + value.len_utf8() as i32 + } + + pub fn utf16SequenceLength(value: char) -> i32 { + value.len_utf16() as i32 + } + + pub fn isAscii(value: char) -> bool { + value.is_ascii() + } + + pub fn isBmp(value: char) -> bool { + value as u32 <= 0xFFFF + } + + pub fn plane(value: char) -> i32 { + (value as u32 >> 16) as i32 + } + + pub fn getNumericValue(value: char) -> f64 { + GetNumericValue(value) + } + + pub fn getUnicodeCategory(value: char) -> i32 { + GetUnicodeCategory(value) + } + + pub fn isControl(value: char) -> bool { + IsControl(value) + } + + pub fn isDigit(value: char) -> bool { + IsDigit(value) + } + + pub fn isLetter(value: char) -> bool { + IsLetter(value) + } + + pub fn isLetterOrDigit(value: char) -> bool { + IsLetterOrDigit(value) + } + + pub fn isLower(value: char) -> bool { + IsLower(value) + } + + pub fn isNumber(value: char) -> bool { + IsNumber(value) + } + + pub fn isPunctuation(value: char) -> bool { + IsPunctuation(value) + } + + pub fn isSeparator(value: char) -> bool { + IsSeparator(value) + } + + pub fn isSymbol(value: char) -> bool { + IsSymbol(value) + } + + pub fn isUpper(value: char) -> bool { + IsUpper(value) + } + + pub fn isWhiteSpace(value: char) -> bool { + IsWhiteSpace(value) + } + + pub fn isValidInt(value: i32) -> bool { + value >= 0 && isValidScalar(value as u32) + } + + pub fn isValidUInt(value: u32) -> bool { + isValidScalar(value) + } + + pub fn toLower(value: char) -> char { + ToLower(value) + } + + pub fn toLowerInvariant(value: char) -> char { + ToLowerInvariant(value) + } + + pub fn toUpper(value: char) -> char { + ToUpper(value) + } + + pub fn toUpperInvariant(value: char) -> char { + ToUpperInvariant(value) + } + + fn tryGetRune(input: &string, index: i32) -> Option { + if index < 0 { + return None; + } + + let target = index as usize; + let mut offset = 0; + for value in input.as_str().chars() { + if target == offset { + return Some(value); + } + + offset += value.len_utf16(); + if target < offset { + return None; + } + } + + None + } + + pub fn getRuneAt(input: string, index: i32) -> char { + match tryGetRune(&input, index) { + Some(value) => value, + None => panic!("Cannot extract a Unicode scalar value from the specified index") + } + } + + pub fn tryGetRuneAt(input: string, index: i32, result: &MutCell) -> bool { + if index < 0 { + panic!("Index was out of range") + } + + let target = index as usize; + let mut offset = 0; + for value in input.as_str().chars() { + if target == offset { + result.set(value); + return true; + } + + offset += value.len_utf16(); + if target < offset { + result.set(zero()); + return false; + } + } + + panic!("Index was out of range") + } + + pub fn parse(input: string) -> char { + let mut chars = input.as_str().chars(); + + match (chars.next(), chars.next()) { + (Some(value), None) => value, + _ => panic!("Input must contain exactly one Unicode scalar value"), + } + } + + pub fn tryParse(input: string, result: &MutCell) -> bool { + let mut chars = input.as_str().chars(); + + match (chars.next(), chars.next()) { + (Some(value), None) => { + result.set(value); + true + } + _ => { + result.set(zero()); + false + } + } + } + + pub fn toString(value: char) -> string { + ofChar(value) + } + + pub fn compareTo(left: char, right: char) -> i32 { + match left.cmp(&right) { + core::cmp::Ordering::Less => -1, + core::cmp::Ordering::Equal => 0, + core::cmp::Ordering::Greater => 1, + } + } + + pub fn equals(left: char, right: char) -> bool { + left == right + } + + pub fn getHashCode(value: char) -> i32 { + value as i32 + } +} diff --git a/src/fable-library-rust/src/String.rs b/src/fable-library-rust/src/String.rs index 0cbf40b2a9..1a12bd6a74 100644 --- a/src/fable-library-rust/src/String.rs +++ b/src/fable-library-rust/src/String.rs @@ -361,6 +361,10 @@ pub mod String_ { fromIter(it) } + pub fn enumerateRunes(s: string) -> Seq { + toSeq(s) + } + // ----------------------------------------------------------- // string implementation // ----------------------------------------------------------- diff --git a/src/fable-library-rust/src/lib.fs b/src/fable-library-rust/src/lib.fs index fc92c08eee..e3145b1911 100644 --- a/src/fable-library-rust/src/lib.fs +++ b/src/fable-library-rust/src/lib.fs @@ -30,6 +30,7 @@ let _imports () = importAll "./Random.rs" importAll "./Reflection.rs" importAll "./RegExp.rs" + importAll "./Rune.rs" importAll "./String.rs" importAll "./TimeOnly.rs" importAll "./TimeSpan.rs" diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 03139f9f9f..8e22dc06c8 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -69,6 +69,7 @@ + diff --git a/tests/Rust/tests/src/RuneTests.fs b/tests/Rust/tests/src/RuneTests.fs new file mode 100644 index 0000000000..633876b4f7 --- /dev/null +++ b/tests/Rust/tests/src/RuneTests.fs @@ -0,0 +1,371 @@ +module Fable.Tests.RuneTests + +open System +open System.Globalization +open System.Text +open Util.Testing + +[] +let ``Rune(Char) constructor works`` () = + Rune('A').Value |> equal 65 + +[] +let ``Rune(Int32) constructor works`` () = + Rune(0x1F600).Value |> equal 0x1F600 + +[] +let ``Rune(Int32) constructor rejects invalid scalar values`` () = + throwsAnyError (fun () -> Rune(-1) |> ignore) + throwsAnyError (fun () -> Rune(0xD800) |> ignore) + throwsAnyError (fun () -> Rune(0x110000) |> ignore) + +[] +let ``Rune(UInt32) constructor works`` () = + Rune(uint32 0x10FFFF).Value |> equal 0x10FFFF + +[] +let ``Rune(UInt32) constructor rejects invalid scalar values`` () = + throwsAnyError (fun () -> Rune(uint32 0xD800) |> ignore) + +[] +let ``Rune(Char, Char) constructor rejects non-surrogate input`` () = + throwsAnyError (fun () -> Rune('a', 'b') |> ignore) + +[] +let ``Rune.TryCreate(Char) works`` () = + Rune.TryCreate('A') |> equal (true, Rune('A')) + +[] +let ``Rune.TryCreate(Char, Char) rejects non-surrogate input`` () = + Rune.TryCreate('a', 'b') |> equal (false, Rune(0)) + +[] +let ``Rune.TryCreate(Int32) works`` () = + Rune.TryCreate(0x1F600) |> equal (true, Rune(0x1F600)) + Rune.TryCreate(-1) |> equal (false, Rune(0)) + +[] +let ``Rune.TryCreate(UInt32) works`` () = + Rune.TryCreate(uint32 0x10FFFF) |> equal (true, Rune(0x10FFFF)) + Rune.TryCreate(uint32 0x110000) |> equal (false, Rune(0)) + +[] +let ``Rune.IsValid(Int32) works`` () = + Rune.IsValid(-1) |> equal false + Rune.IsValid(0) |> equal true + Rune.IsValid(0xD7FF) |> equal true + Rune.IsValid(0xD800) |> equal false + Rune.IsValid(0xDFFF) |> equal false + Rune.IsValid(0xE000) |> equal true + Rune.IsValid(0x10FFFF) |> equal true + Rune.IsValid(0x110000) |> equal false + +[] +let ``Rune.IsValid(UInt32) works`` () = + Rune.IsValid(uint32 0xD800) |> equal false + Rune.IsValid(uint32 0x10FFFF) |> equal true + Rune.IsValid(UInt32.MaxValue) |> equal false + +[] +let ``Rune.Value works`` () = + Rune('A').Value |> equal 65 + Rune(0x1F600).Value |> equal 0x1F600 + Unchecked.defaultof.Value |> equal 0 + +[] +let ``Rune.Utf8SequenceLength works`` () = + Rune('A').Utf8SequenceLength |> equal 1 + Rune('\u00E9').Utf8SequenceLength |> equal 2 + Rune('\u20AC').Utf8SequenceLength |> equal 3 + Rune(0x1F600).Utf8SequenceLength |> equal 4 + +[] +let ``Rune.Utf16SequenceLength works`` () = + Rune('A').Utf16SequenceLength |> equal 1 + Rune(0x1F600).Utf16SequenceLength |> equal 2 + +[] +let ``Rune.IsAscii works`` () = + Rune(0x7F).IsAscii |> equal true + Rune(0x80).IsAscii |> equal false + +[] +let ``Rune.IsBmp works`` () = + Rune(0xFFFF).IsBmp |> equal true + Rune(0x10000).IsBmp |> equal false + +[] +let ``Rune.Plane works`` () = + Rune(0xFFFF).Plane |> equal 0 + Rune(0x10000).Plane |> equal 1 + Rune(0x10FFFF).Plane |> equal 16 + +[] +let ``Rune.ReplacementChar works`` () = + Rune.ReplacementChar |> equal (Rune(0xFFFD)) + +[] +let ``Rune.GetUnicodeCategory works`` () = + Rune.GetUnicodeCategory(Rune('A')) |> equal UnicodeCategory.UppercaseLetter + +[] +let ``Rune.GetNumericValue works`` () = + Rune.GetNumericValue(Rune('4')) |> equal 4.0 + +[] +let ``Rune.IsControl works`` () = + Rune.IsControl(Rune('\u0000')) |> equal true + +[] +let ``Rune.IsDigit works`` () = + Rune.IsDigit(Rune('4')) |> equal true + +[] +let ``Rune.IsLetter works`` () = + Rune.IsLetter(Rune('A')) |> equal true + Rune.IsLetter(Rune('家')) |> equal true + +[] +let ``Rune.IsLetterOrDigit works`` () = + Rune.IsLetterOrDigit(Rune('A')) |> equal true + Rune.IsLetterOrDigit(Rune('-')) |> equal false + +[] +let ``Rune.IsLower works`` () = + Rune.IsLower(Rune('a')) |> equal true + +[] +let ``Rune.IsNumber works`` () = + Rune.IsNumber(Rune('4')) |> equal true + +[] +let ``Rune.IsPunctuation works`` () = + Rune.IsPunctuation(Rune('.')) |> equal true + +[] +let ``Rune.IsSeparator works`` () = + Rune.IsSeparator(Rune(' ')) |> equal true + +[] +let ``Rune.IsSymbol works`` () = + Rune.IsSymbol(Rune('+')) |> equal true + +[] +let ``Rune.IsUpper works`` () = + Rune.IsUpper(Rune('A')) |> equal true + +[] +let ``Rune.IsWhiteSpace works`` () = + Rune.IsWhiteSpace(Rune('\n')) |> equal true + +[] +let ``Rune.ToLower works`` () = + Rune.ToLower(Rune('A'), CultureInfo.InvariantCulture) |> equal (Rune('a')) + +[] +let ``Rune.ToLowerInvariant works`` () = + Rune.ToLowerInvariant(Rune('A')) |> equal (Rune('a')) + +[] +let ``Rune.ToUpper works`` () = + Rune.ToUpper(Rune('b'), CultureInfo.InvariantCulture) |> equal (Rune('B')) + +[] +let ``Rune.ToUpperInvariant works`` () = + Rune.ToUpperInvariant(Rune('b')) |> equal (Rune('B')) + +[] +let ``Rune.ToString works`` () = + Rune('A').ToString() |> equal "A" + Rune(0x1F600).ToString() |> equal "\uD83D\uDE00" + +[] +let ``Rune string conversion works`` () = + string (Rune(0x1F600)) |> equal "\uD83D\uDE00" + +[] +let ``Rune.GetRuneAt works`` () = + let input = "A\uD83D\uDE00Z" + + Rune.GetRuneAt(input, 0) |> equal (Rune('A')) + Rune.GetRuneAt(input, 1) |> equal (Rune(0x1F600)) + Rune.GetRuneAt(input, 3) |> equal (Rune('Z')) + +[] +let ``Rune.GetRuneAt rejects invalid positions`` () = + let input = "A\uD83D\uDE00Z" + + throwsAnyError (fun () -> Rune.GetRuneAt(input, -1) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt(input, 2) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt(input, 4) |> ignore) + +[] +let ``Rune.TryGetRuneAt works`` () = + let input = "A\uD83D\uDE00Z" + + Rune.TryGetRuneAt(input, 0) |> equal (true, Rune('A')) + Rune.TryGetRuneAt(input, 1) |> equal (true, Rune(0x1F600)) + Rune.TryGetRuneAt(input, 2) |> equal (false, Rune(0)) + Rune.TryGetRuneAt(input, 3) |> equal (true, Rune('Z')) + +[] +let ``Rune.TryGetRuneAt rejects out of range positions`` () = + let input = "A\uD83D\uDE00Z" + + throwsAnyError (fun () -> Rune.TryGetRuneAt(input, -1) |> ignore) + throwsAnyError (fun () -> Rune.TryGetRuneAt(input, 4) |> ignore) + +[] +let ``Rune equality operator works`` () = + Rune('a') = Rune('a') |> equal true + Rune('a') = Rune('b') |> equal false + +[] +let ``Rune inequality operator works`` () = + Rune('a') <> Rune('b') |> equal true + +[] +let ``Rune less than operator works`` () = + Rune('a') < Rune('b') |> equal true + +[] +let ``Rune less than or equal operator works`` () = + Rune('a') <= Rune('a') |> equal true + +[] +let ``Rune greater than operator works`` () = + Rune('b') > Rune('a') |> equal true + +[] +let ``Rune greater than or equal operator works`` () = + Rune('b') >= Rune('b') |> equal true + +[] +let ``Rune.CompareTo works`` () = + Rune('a').CompareTo(Rune('b')) |> equal -1 + Rune('a').CompareTo(Rune('a')) |> equal 0 + Rune('b').CompareTo(Rune('a')) |> equal 1 + +[] +let ``Rune comparison works`` () = + compare (Rune('a')) (Rune('b')) |> equal -1 + compare (Rune('a')) (Rune('a')) |> equal 0 + compare (Rune('b')) (Rune('a')) |> equal 1 + +[] +let ``Rune.Equals works`` () = + Rune('a').Equals(Rune('a')) |> equal true + Rune('a').Equals(Rune('b')) |> equal false + +[] +let ``Rune.GetHashCode works`` () = + Rune('a').GetHashCode() |> equal 97 + Rune(0x1F600).GetHashCode() |> equal 0x1F600 + +[] +let ``Rune.GetRuneAt works with BMP characters`` () = + let rune = Rune.GetRuneAt("ABC", 1) + rune.Value |> equal 0x42 + rune.ToString() |> equal "B" + +[] +let ``Rune.GetRuneAt works with supplementary characters`` () = + let rune = Rune.GetRuneAt("𠀀", 0) + rune.Value |> equal 0x20000 + rune.ToString() |> equal "𠀀" + +[] +let ``Rune.GetRuneAt uses UTF-16 code unit indexes`` () = + Rune.GetRuneAt("A𠀀B", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("A𠀀B", 1).ToString() + |> equal "𠀀" + + Rune.GetRuneAt("A𠀀B", 3).ToString() + |> equal "B" + +[] +let ``Rune.GetRuneAt throws when index points inside a surrogate pair`` () = + throwsAnyError <| fun () -> + Rune.GetRuneAt("A𠀀B", 2) + |> ignore + +[] +let ``Rune.GetRuneAt throws when index is out of range`` () = + throwsAnyError (fun () -> Rune.GetRuneAt("ABC", -1) |> ignore) + throwsAnyError (fun () -> Rune.GetRuneAt("ABC", 3) |> ignore) + +[] +let ``Rune.GetRuneAt returns ReplacementChar for unpaired surrogate characters`` () = + Rune.GetRuneAt("\uD800", 0) |> equal Rune.ReplacementChar + Rune.GetRuneAt("\uDC00", 0) |> equal Rune.ReplacementChar + +[] +let ``Rune.Value returns the Unicode scalar value`` () = + Rune.GetRuneAt("A", 0).Value + |> equal 0x41 + + Rune.GetRuneAt("𠀀", 0).Value + |> equal 0x20000 + +[] +let ``Rune.Utf16SequenceLength returns the number of UTF-16 code units`` () = + Rune.GetRuneAt("A", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("𠀀", 0).Utf16SequenceLength + |> equal 2 + +[] +let ``Rune.Utf16SequenceLength works at the BMP boundary`` () = + Rune.GetRuneAt("\uFFFF", 0).Utf16SequenceLength + |> equal 1 + + Rune.GetRuneAt("\U00010000", 0).Utf16SequenceLength + |> equal 2 + +[] +let ``Rune.ToString returns the represented Unicode scalar as a string`` () = + Rune.GetRuneAt("A", 0).ToString() + |> equal "A" + + Rune.GetRuneAt("𠀀", 0).ToString() + |> equal "𠀀" + +[] +let ``F# string conversion works with Rune`` () = + Rune.GetRuneAt("A", 0) + |> string + |> equal "A" + + Rune.GetRuneAt("𠀀", 0) + |> string + |> equal "𠀀" + +[] +let ``String.EnumerateRunes enumerates Unicode scalar values`` () = + "A𠀀B".EnumerateRunes() + |> Seq.map (fun rune -> rune.Value) + |> Seq.toList + |> equal [ 0x41; 0x20000; 0x42 ] + +[] +let ``String.EnumerateRunes works with Rune string conversion`` () = + "A𠀀B".EnumerateRunes() + |> Seq.map string + |> Seq.toList + |> equal [ "A"; "𠀀"; "B" ] + +[] +let ``String.EnumerateRunes replaces unpaired surrogates`` () = + // .NET String.EnumerateRunes replaces invalid UTF-16 sequences with U+FFFD + // https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.replacementchar + + // Note: For Rust, this only works for string literals (replaced at compile time) + let input = "\uD800A\uDC00" + + input.EnumerateRunes() + |> Seq.map _.Value + |> Seq.toList + |> equal [ 0xFFFD; 0x41; 0xFFFD ] From ec8d4c270683a6150389d77f7de6c22563c0079d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:33:08 -0700 Subject: [PATCH 097/322] fix(rust): Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) --- src/Fable.Transforms/Rust/Replacements.fs | 1646 ++++++++------------- src/Fable.Transforms/Transforms.Util.fs | 3 + src/fable-library-rust/src/Decimal.rs | 22 +- tests/Rust/tests/src/ArithmeticTests.fs | 107 +- 4 files changed, 751 insertions(+), 1027 deletions(-) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 9ccf6bc93e..fc76502f69 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -95,12 +95,8 @@ let makeStaticMemberCall com r t (i: CallInfo) moduleName memberName args = let makeStaticFieldCall com r t moduleName entityName memberName = let memberName = entityName + "::" + memberName - Helper.LibCall(com, moduleName, memberName, t, [], ?isModuleMember = Some(false), ?loc = r) -let makeLibCall com r t (i: CallInfo) moduleName memberName args = - Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, i.GenericArgs, ?loc = r) - let makeLibModuleCall com r t (i: CallInfo) moduleName memberName (thisArg: Expr option) (args: Expr list) = let args, argTypes = match thisArg with @@ -109,6 +105,24 @@ let makeLibModuleCall com r t (i: CallInfo) moduleName memberName (thisArg: Expr Helper.LibCall(com, moduleName, memberName, t, args, argTypes, i.GenericArgs, ?loc = r) +let makeLibCall com r t (i: CallInfo) moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?loc = r) + +let libCallThis com r t (i: CallInfo) moduleName memberName thisArg args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) + +let libCallCons com r t (i: CallInfo) moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) + +let libCallTyped com r t moduleName memberName args argTypes = + Helper.LibCall(com, moduleName, memberName, t, args, argTypes, ?loc = r) + +let libCall com r t moduleName memberName args = + Helper.LibCall(com, moduleName, memberName, t, args, ?loc = r) + +let libValue com r t moduleName memberName args = + Helper.LibValue(com, moduleName, memberName, t) // TODO: range + let makeGlobalIdent (ident: string, memb: string, typ: Type) = makeTypedIdentExpr typ (ident + "::" + memb) @@ -117,7 +131,6 @@ let makeUniqueIdent com ctx t name = let makeDecimal com r t (x: decimal) = let str = x.ToString(System.Globalization.CultureInfo.InvariantCulture) - Helper.LibCall(com, "Decimal", "fromString", t, [ makeStrConst str ], isConstructor = true, ?loc = r) let makeRef (value: Expr) = @@ -134,7 +147,6 @@ let setRefCell com r (expr: Expr) (value: Expr) = let makeRefCell com r genArg args = let typ = makeFSharpCoreType [ genArg ] Types.refCell - Helper.LibCall(com, "Native", "refCell", typ, args, isConstructor = true, ?loc = r) let makeRefCellFromValue com r (value: Expr) = makeRefCell com r value.Type [ value ] @@ -190,11 +202,11 @@ let convertTo com (ctx: Context) r t (args: Expr list) = | Boolean -> match sourceType with | Boolean -> args.Head - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toBoolean", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toBoolean", t, args, ?loc = r) - | Number(_kind, _) -> Helper.LibCall(com, "Convert", "toBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "Convert", "toBoolean", t, args, ?loc = r) - | String -> Helper.LibCall(com, "Convert", "parseBoolean", t, args, ?loc = r) + | Number(Decimal, _) -> libCall com r t "Decimal" "toBoolean" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toBoolean" args + | Number(_kind, _) -> libCall com r t "Convert" "toBoolean" args + | Char -> libCall com r t "Convert" "toBoolean" args + | String -> libCall com r t "Convert" "parseBoolean" args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) @@ -202,39 +214,39 @@ let convertTo com (ctx: Context) r t (args: Expr list) = | Char -> match sourceType with | Char -> args.Head - | String -> Helper.LibCall(com, "Convert", "parseChar", t, args, ?loc = r) - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toChar", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toChar", t, args, ?loc = r) + | String -> libCall com r t "Convert" "parseChar" args + | Number(Decimal, _) -> libCall com r t "Decimal" "toChar" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toChar" args | Number(_kind, _) -> let code = TypeCast(args.Head, UInt32.Number) - Helper.LibCall(com, "Char", "fromCharCode", t, [ code ]) + libCall com r t "Char" "fromCharCode" [ code ] | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) | Number(Decimal, _) -> match sourceType with - | Array(Number(Int32, _), _) -> Helper.LibCall(com, "Decimal", "fromIntArray", t, args, ?loc = r) - | Boolean -> Helper.LibCall(com, "Decimal", "fromBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "Decimal", "fromChar", t, args, ?loc = r) - | String -> Helper.LibCall(com, "Decimal", "fromString", t, args, ?loc = r) - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toDecimal", t, args, ?loc = r) + | Array(Number(Int32, _), _) -> libCall com r t "Decimal" "fromIntArray" args + | Boolean -> libCall com r t "Decimal" "fromBoolean" args + | Char -> libCall com r t "Decimal" "fromChar" args + | String -> libCall com r t "Decimal" "fromString" args + | Number(BigInt, _) -> libCall com r t "BigInt" "toDecimal" args | Number(kind, _) -> let meth = "from" + kind.ToString() - Helper.LibCall(com, "Decimal", meth, t, args, ?loc = r) + libCall com r t "Decimal" meth args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) | Number(BigInt, _) -> match sourceType with - | Array(Number(UInt8, _), _) -> Helper.LibCall(com, "BigInt", "fromByteArray", t, args, ?loc = r) - | Boolean -> Helper.LibCall(com, "BigInt", "fromBoolean", t, args, ?loc = r) - | Char -> Helper.LibCall(com, "BigInt", "fromChar", t, args, ?loc = r) - | String -> Helper.LibCall(com, "BigInt", "fromString", t, args, ?loc = r) + | Array(Number(UInt8, _), _) -> libCall com r t "BigInt" "fromByteArray" args + | Boolean -> libCall com r t "BigInt" "fromBoolean" args + | Char -> libCall com r t "BigInt" "fromChar" args + | String -> libCall com r t "BigInt" "fromString" args | Number(kind, _) -> let meth = "from" + kind.ToString() - Helper.LibCall(com, "BigInt", meth, t, args, ?loc = r) + libCall com r t "BigInt" meth args | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" TypeCast(args.Head, t) @@ -250,13 +262,13 @@ let convertTo com (ctx: Context) r t (args: Expr list) = TypeCast(code, t) | String -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "Convert", meth, t, args, ?loc = r) + libCall com r t "Convert" meth args | Number(Decimal, _) -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "Decimal", meth, t, args, ?loc = r) + libCall com r t "Decimal" meth args | Number(BigInt, _) -> let meth = "to" + kind.ToString() - Helper.LibCall(com, "BigInt", meth, t, args, ?loc = r) + libCall com r t "BigInt" meth args | Number _ -> TypeCast(args.Head, t) | _ -> addWarning com ctx.InlinePath r "Unsupported conversion" @@ -274,8 +286,8 @@ let rec toString com (ctx: Context) r (args: Expr list) = | head :: tail -> match head.Type with | String -> head - | Char -> Helper.LibCall(com, "String", "ofChar", String, [ head ]) - | Boolean -> Helper.LibCall(com, "String", "ofBoolean", String, [ head ]) + | Char -> libCall com r String "String" "ofChar" [ head ] + | Boolean -> libCall com r String "String" "ofBoolean" [ head ] // Rust has no Display for tuples, and the orphan rule rules out adding // one. Building the text here also matches .NET more closely than a // derived Debug would: each element goes through its own ToString, so a @@ -290,14 +302,14 @@ let rec toString com (ctx: Context) r (args: Expr list) = |> List.reduce (fun acc part -> add (add acc (makeStrConst ", ")) part) add (add (makeStrConst "(") separated) (makeStrConst ")") - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toString", String, args) - | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toString", String, args) + | Number(BigInt, _) -> libCall com r String "BigInt" "toString" args + | Number(Decimal, _) -> libCall com r String "Decimal" "toString" args // | Array _ | List _ -> - // Helper.LibCall(com, "Types", "seqToString", String, [head], ?loc=r) + // libCall com r String "Types" "seqToString" [ head ] // | DeclaredType(ent, _) when ent.IsFSharpUnion || ent.IsFSharpRecord || ent.IsValueType -> // Helper.InstanceCall(head, "toString", String, [], ?loc=r) // | DeclaredType(ent, _) -> - | _ -> Helper.LibCall(com, "String", "toString", String, [ head ]) + | _ -> libCall com r String "String" "toString" [ head ] let toRoundInt com (ctx: Context) r t i (args: Expr list) = let sourceType = args.Head.Type @@ -315,35 +327,35 @@ let toRadixInt com (ctx: Context) r t i (args: Expr list) = match t with | Number(kind, _) -> let meth = "to" + kind.ToString() + "_radix" - Helper.LibCall(com, "Convert", meth, t, args, ?loc = r) + libCall com r t "Convert" meth args | _ -> FableError $"Unexpected conversion %s{i.CompiledName}" |> raise let toArray com t (expr: Expr) = match expr.Type with | Array _ -> expr - | List _ -> Helper.LibCall(com, "List", "toArray", t, [ expr ]) - | String -> Helper.LibCall(com, "String", "toCharArray", t, [ expr ]) - | IEnumerable -> Helper.LibCall(com, "Seq", "toArray", t, [ expr ]) + | List _ -> libCall com None t "List" "toArray" [ expr ] + | String -> libCall com None t "String" "toCharArray" [ expr ] + | IEnumerable -> libCall com None t "Seq" "toArray" [ expr ] | _ -> TypeCast(expr, t) let toList com t (expr: Expr) = match expr.Type with | List _ -> expr - | Array _ -> Helper.LibCall(com, "List", "ofArray", t, [ expr ]) + | Array _ -> libCall com None t "List" "ofArray" [ expr ] | String -> - let chars = Helper.LibCall(com, "String", "toSeq", t, [ expr ]) - Helper.LibCall(com, "List", "ofSeq", t, [ chars ]) - | IEnumerable -> Helper.LibCall(com, "List", "ofSeq", t, [ expr ]) + let chars = libCall com None t "String" "toSeq" [ expr ] + libCall com None t "List" "ofSeq" [ chars ] + | IEnumerable -> libCall com None t "List" "ofSeq" [ expr ] | _ -> TypeCast(expr, t) let toSeq com t (expr: Expr) = match expr.Type with | IEnumerable -> expr - | List _ -> Helper.LibCall(com, "Seq", "ofList", t, [ expr ]) - | Array _ -> Helper.LibCall(com, "Seq", "ofArray", t, [ expr ]) - | Builtin(FSharpMap _) -> Helper.LibCall(com, "Map", "toEnumerable", t, [ expr ]) - | Builtin(FSharpSet _) -> Helper.LibCall(com, "Set", "toEnumerable", t, [ expr ]) - | String -> Helper.LibCall(com, "String", "toSeq", t, [ expr ]) + | List _ -> libCall com None t "Seq" "ofList" [ expr ] + | Array _ -> libCall com None t "Seq" "ofArray" [ expr ] + | Builtin(FSharpMap _) -> libCall com None t "Map" "toEnumerable" [ expr ] + | Builtin(FSharpSet _) -> libCall com None t "Set" "toEnumerable" [ expr ] + | String -> libCall com None t "String" "toSeq" [ expr ] | _ -> TypeCast(expr, t) let emitRawString (s: string) = $"\"%s{s}\"" |> emitExpr None String [] @@ -362,7 +374,7 @@ let emitFormat (com: ICompiler) r t (args: Expr list) macro = | _ -> (emitRawString "{0}") :: args let unboxedArgs = args |> FSharp2Fable.Util.unboxBoxedArgs - Helper.LibCall(com, "String", macro, t, unboxedArgs) + libCall com r t "String" macro unboxedArgs let getMut expr = Helper.InstanceCall(expr, "get_mut", expr.Type, []) @@ -403,8 +415,7 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = | _ -> binOp BinaryMinus left right | Operators.multiply, [ left; right ] -> binOp BinaryMultiply left right | Operators.division, [ left; right ] -> binOp BinaryDivide left right - | Operators.divideByInt, [ left; right ] -> - Helper.LibCall(com, "Native", "divideByInt", t, [ left; right ], argTypes, ?loc = r) + | Operators.divideByInt, [ left; right ] -> libCallTyped com r t "Native" "divideByInt" [ left; right ] argTypes | Operators.modulus, [ left; right ] -> binOp BinaryModulus left right | Operators.leftShift, [ left; right ] -> binOp BinaryShiftLeft left right |> truncateUnsigned // See #1530 | Operators.rightShift, [ left; right ] -> @@ -427,7 +438,7 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = match argTypes with // | Number(BigInt as kind,_)::_ -> - // Helper.LibCall(com, "BigInt", opName, t, args, argTypes, ?loc=r) + // libCallTyped com r t "BigInt" opName [ left; right ] argTypes | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan | BclRune) :: _ -> nativeOp opName argTypes args | Builtin(FSharpSet _) :: _ -> @@ -437,10 +448,10 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = | Operators.subtraction -> "difference" | _ -> opName - Helper.LibCall(com, "Set", methName, t, args, argTypes, ?loc = r) + libCallTyped com r t "Set" methName args argTypes // | Builtin (FSharpMap _)::_ -> // let mangledName = Naming.buildNameWithoutSanitationFrom "FSharpMap" true opName overloadSuffix.Value - // Helper.LibCall(com, "Map", mangledName, t, args, argTypes, ?loc=r) + // libCallTyped com r t "Map" mangledName args argTypes | CustomOp com ctx r t opName args e -> e | _ -> nativeOp opName argTypes args @@ -469,13 +480,13 @@ let referenceHash (com: ICompiler) ctx r (arg: Expr) = | Char | String | Number _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) - | _ -> Helper.LibCall(com, "Native", "referenceHash", Int32.Number, [ makeRef arg ], ?loc = r) + | _ -> libCall com r Int32.Number "Native" "referenceHash" [ makeRef arg ] let getHashCode (com: ICompiler) ctx r (arg: Expr) = match arg.Type with | HasReferenceEquality com _ -> referenceHash com ctx r arg - | Builtin BclRune -> Helper.LibCall(com, "Rune", "getHashCode", Int32.Number, [ arg ], ?loc = r) - | Char -> Helper.LibCall(com, "Char", "GetHashCode", Int32.Number, [ arg ], ?loc = r) + | Builtin BclRune -> libCall com r Int32.Number "Rune" "getHashCode" [ arg ] + | Char -> libCall com r Int32.Number "Char" "GetHashCode" [ arg ] | _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) let objectHash (com: ICompiler) ctx r (arg: Expr) = @@ -486,14 +497,14 @@ let objectHash (com: ICompiler) ctx r (arg: Expr) = let referenceEquals (com: ICompiler) ctx r (left: Expr) (right: Expr) = match left, right with | Value(Null _, _), o - | o, Value(Null _, _) -> Helper.LibCall(com, "Native", "is_null", Boolean, [ makeRef o ], ?loc = r) + | o, Value(Null _, _) -> libCall com r Boolean "Native" "is_null" [ makeRef o ] | _ -> match left.Type with | Boolean | Char | String | Number _ -> makeEqOp r left right BinaryEqual - | _ -> Helper.LibCall(com, "Native", "referenceEquals", Boolean, [ makeRef left; makeRef right ], ?loc = r) + | _ -> libCall com r Boolean "Native" "referenceEquals" [ makeRef left; makeRef right ] let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = let t = Boolean @@ -504,14 +515,14 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | String | Number _ | Builtin(FSharpChoice _ | FSharpResult _) -> makeEqOp r left right BinaryEqual - | Builtin kind -> Helper.LibCall(com, coreModFor kind, "equals", t, [ left; right ], ?loc = r) + | Builtin kind -> libCall com r t (coreModFor kind) "equals" [ left; right ] | Array(_, ResizeArray) -> referenceEquals com ctx r left right - | Array _ -> Helper.LibCall(com, "Array", "equals", t, [ left; right ], ?loc = r) - | List _ -> Helper.LibCall(com, "List", "equals", t, [ left; right ], ?loc = r) - | IEnumerable -> Helper.LibCall(com, "Seq", "equals", t, [ left; right ], ?loc = r) + | Array _ -> libCall com r t "Array" "equals" [ left; right ] + | List _ -> libCall com r t "List" "equals" [ left; right ] + | IEnumerable -> libCall com r t "Seq" "equals" [ left; right ] // System.Type is erased to a boxed reflection object (dyn Any, no PartialEq), // so type-identity equality is compared on the carried TypeId at runtime. - | MetaType -> Helper.LibCall(com, "Reflection", "typeEquals", t, [ left; right ], ?loc = r) + | MetaType -> libCall com r t "Reflection" "typeEquals" [ left; right ] | HasReferenceEquality com _ -> referenceEquals com ctx r left right | Nullable _ -> // transforms null checks into option tests @@ -520,7 +531,7 @@ let equals (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Value(NewOption(None, _, _), _), expr -> Test(expr, OptionTest false, r) | _ -> makeEqOp r left right BinaryEqual | _ -> - // Helper.LibCall(com, "Native", "equals", t, [left; right], ?loc=r) + // libCall com r t "Native" "equals" [ left; right ] makeEqOp r left right BinaryEqual /// Compare function that will call Util.compare or instance `CompareTo` as appropriate @@ -532,12 +543,12 @@ let compare (com: ICompiler) ctx r (left: Expr) (right: Expr) = | Char | String | Number _ - | Builtin(FSharpChoice _ | FSharpResult _) -> Helper.LibCall(com, "Native", "compare", t, [ left; right ], ?loc = r) - | Builtin kind -> Helper.LibCall(com, coreModFor kind, "compareTo", t, [ left; right ], ?loc = r) - | Array _ -> Helper.LibCall(com, "Array", "compareTo", t, [ left; right ], ?loc = r) - | List _ -> Helper.LibCall(com, "List", "compareTo", t, [ left; right ], ?loc = r) - | IEnumerable -> Helper.LibCall(com, "Seq", "compareTo", t, [ left; right ], ?loc = r) - | _ -> Helper.LibCall(com, "Native", "compare", t, [ left; right ], ?loc = r) + | Builtin(FSharpChoice _ | FSharpResult _) -> libCall com r t "Native" "compare" [ left; right ] + | Builtin kind -> libCall com r t (coreModFor kind) "compareTo" [ left; right ] + | Array _ -> libCall com r t "Array" "compareTo" [ left; right ] + | List _ -> libCall com r t "List" "compareTo" [ left; right ] + | IEnumerable -> libCall com r t "Seq" "compareTo" [ left; right ] + | _ -> libCall com r t "Native" "compare" [ left; right ] /// Boolean comparison operators like <, >, <=, >= let booleanCompare (com: ICompiler) ctx r (left: Expr) (right: Expr) op = @@ -616,7 +627,7 @@ let makeSet (com: ICompiler) ctx r t args genArg = | [ ExprType(Array _) ] -> "ofArray" | _ -> "ofSeq" - Helper.LibCall(com, "Set", meth, t, args, ?loc = r) + libCall com r t "Set" meth args /// Adds comparer as last argument for map creator methods let makeMap (com: ICompiler) ctx r t args genArg = @@ -628,7 +639,7 @@ let makeMap (com: ICompiler) ctx r t args genArg = | [ ExprType(Array _) ] -> "ofArray" | _ -> "ofSeq" - Helper.LibCall(com, "Map", Naming.lowerFirst meth, t, args, ?loc = r) + libCall com r t "Map" (Naming.lowerFirst meth) args // let makeDictionaryWithComparer com r t sourceSeq comparer = // Helper.LibCall(com, "MutableMap", "Dictionary", t, [sourceSeq; comparer], isConstructor=true, ?loc=r) @@ -652,31 +663,31 @@ let rec getZero (com: ICompiler) (ctx: Context) (t: Type) = | Nullable(genArg, true) -> NewOption(None, genArg, false) |> makeValue None | Nullable(genArg, false) -> Null t |> makeValue None | Boolean -> makeBoolConst false - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "zero", t, []) - | Number(Decimal, _) -> Helper.LibValue(com, "Decimal", "Zero", t) + | Number(BigInt, _) -> libCall com None t "BigInt" "zero" [] + | Number(Decimal, _) -> libValue com None t "Decimal" "Zero" [] | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None | Char -> CharConstant '\u0000' |> makeValue None | String -> Null t |> makeValue None | Array(typ, _) -> makeArray typ [] | List genArg -> NewList(None, genArg) |> makeValue None - | Builtin BclDateTime -> Helper.LibCall(com, "DateTime", "zero", t, []) - | Builtin BclDateTimeOffset -> Helper.LibCall(com, "DateTimeOffset", "zero", t, []) - | Builtin BclDateOnly -> Helper.LibCall(com, "DateOnly", "zero", t, []) - | Builtin BclTimeOnly -> Helper.LibCall(com, "TimeOnly", "zero", t, []) - | Builtin BclRune -> Helper.LibCall(com, "Rune", "zero", t, []) - | Builtin BclTimeSpan -> Helper.LibValue(com, "TimeSpan", "zero", t) + | Builtin BclDateTime -> libCall com None t "DateTime" "zero" [] + | Builtin BclDateTimeOffset -> libCall com None t "DateTimeOffset" "zero" [] + | Builtin BclDateOnly -> libCall com None t "DateOnly" "zero" [] + | Builtin BclTimeOnly -> libCall com None t "TimeOnly" "zero" [] + | Builtin BclRune -> libCall com None t "Rune" "zero" [] + | Builtin BclTimeSpan -> libValue com None t "TimeSpan" "zero" [] | Builtin(FSharpSet genArg) -> makeSet com ctx None t [] genArg - | Builtin BclGuid -> Helper.LibValue(com, "Guid", "empty", t) + | Builtin BclGuid -> libValue com None t "Guid" "empty" [] | Builtin(BclKeyValuePair(k, v)) -> makeTuple None true [ getZero com ctx k; getZero com ctx v ] // | ListSingleton(CustomOp com ctx None t "get_Zero" [] e) -> e | IsReferenceType com _ -> Null t |> makeValue None - | _ -> Helper.LibCall(com, "Native", "getZero", t, []) + | _ -> libCall com None t "Native" "getZero" [] let getOne (com: ICompiler) (ctx: Context) (t: Type) = match t with | Boolean -> makeBoolConst true - | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "one", t, []) - | Number(Decimal, _) -> Helper.LibValue(com, "Decimal", "One", t) + | Number(BigInt, _) -> libCall com None t "BigInt" "one" [] + | Number(Decimal, _) -> libValue com None t "Decimal" "One" [] | Number(kind, uom) -> NumberConstant(NumberValue.GetOne kind, uom) |> makeValue None // | ListSingleton(CustomOp com ctx None t "get_One" [] e) -> e | _ -> makeIntConst 1 @@ -743,16 +754,16 @@ let makeAddFunction (com: ICompiler) ctx t = // | Some injectInfo -> injectArgInner args injectInfo let tryOp com r t op args = - Helper.LibCall(com, "Option", "tryOp", t, op :: args, ?loc = r) + libCall com r t "Option" "tryOp" (op :: args) let tryCoreOp com r t coreModule coreMember args = - let op = Helper.LibValue(com, coreModule, coreMember, Any) + let op = libValue com r Any coreModule coreMember [] tryOp com r t op args let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.DeclaringEntityFullName, i.CompiledName with | _, UniversalFableCoreHelpers com ctx r t i args error expr -> Some expr - | "Fable.Core.Reflection", meth -> Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some + | "Fable.Core.Reflection", meth -> libCall com r t "Reflection" meth args |> Some | "Fable.Core.Compiler", meth -> match meth with | "version" -> makeStrConst Literals.VERSION |> Some @@ -793,8 +804,8 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let refCells (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "get_Value", Some callee, _ -> getRefCell com r t callee |> Some - | "set_Value", Some callee, [ value ] -> setRefCell com r callee value |> Some + | "get_Value", Some c, _ -> getRefCell com r t c |> Some + | "set_Value", Some c, [ value ] -> setRefCell com r c value |> Some | _ -> None let getMemberName isStatic (i: CallInfo) = @@ -830,9 +841,9 @@ let getModuleAndMemberName (i: CallInfo) (thisArg: Expr option) = let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with - | Some callee -> + | Some c -> let memberName = getMemberName false i - makeInstanceCall r t i callee memberName args |> Some + makeInstanceCall r t i c memberName args |> Some | None -> let moduleName, memberName = getModuleAndMemberName i thisArg makeStaticLibCall com r t i moduleName memberName args |> Some @@ -840,8 +851,7 @@ let bclType (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt let fsharpModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let moduleName, memberName = getModuleAndMemberName i thisArg - Helper.LibCall(com, moduleName, memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i moduleName memberName args |> Some // Maps a .NET standard numeric format specifier (e.g. "X4", "D5", "F2") to a // Rust format spec body { flags; width; precision; type } so it can be merged @@ -1008,7 +1018,7 @@ let makeRustFormatString interpolated (fmt: string) = rustFmt, argCount let makeRustFormatExpr com r t (fmt: string) args macro = - let macroExpr = Helper.LibValue(com, "String", macro, Any) + let macroExpr = libValue com r Any "String" macro [] let rustFmt, argCount = makeRustFormatString false fmt let argCount = argCount + 1 + (List.length args) // +1 is for fmt let applied = Extended(Curry(macroExpr, argCount), r) @@ -1017,8 +1027,8 @@ let makeRustFormatExpr com r t (fmt: string) args macro = let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // | "get_Value", Some callee, _ -> - // callee |> Some //TODO: + // | "get_Value", Some c, _ -> + // c |> Some // TODO: | ("PrintFormatToString" | "PrintFormatToStringThen"), None, [ StringConst fmt ] -> "sprintf!" |> makeRustFormatExpr com r t fmt [] |> Some | ("PrintFormatToString" | "PrintFormatToStringThen"), None, [ MaybeCasted(template) ] -> template |> Some @@ -1043,16 +1053,16 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op "failwithf!" |> makeRustFormatExpr com r t fmt [] |> Some | "PrintFormatToStringThenFail", None, _ -> "failwithf!" |> emitFormat com r t args |> Some | "PrintFormatToStringBuilder", None, [ sb; StringConst fmt ] -> - let cont = Helper.LibCall(com, "Util", "bprintf", t, [ sb ]) + let cont = libCall com r t "Util" "bprintf" [ sb ] "kprintf!" |> makeRustFormatExpr com r t fmt [ cont ] |> Some | "PrintFormatToStringBuilder", None, [ sb; MaybeCasted(template) ] -> - let cont = Helper.LibCall(com, "Util", "bprintf", t, [ sb ]) + let cont = libCall com r t "Util" "bprintf" [ sb ] Helper.Application(cont, t, [ template ], ?loc = r) |> Some | "PrintFormatToStringBuilderThen", None, [ cont; sb; StringConst fmt ] -> - let cont = Helper.LibCall(com, "Util", "kbprintf", t, [ cont; sb ]) + let cont = libCall com r t "Util" "kbprintf" [ cont; sb ] "kprintf!" |> makeRustFormatExpr com r t fmt [ cont ] |> Some | "PrintFormatToStringBuilderThen", None, [ cont; sb; MaybeCasted(template) ] -> - let cont = Helper.LibCall(com, "Util", "kbprintf", t, [ cont; sb ]) + let cont = libCall com r t "Util" "kbprintf" [ cont; sb ] Helper.Application(cont, t, [ template ], ?loc = r) |> Some | ".ctor", _, (StringConst fmt) :: (Value(NewArray(ArrayValues templateArgs, _, _), _)) :: _ -> let rustFmt, _argCount = makeRustFormatString true fmt @@ -1076,9 +1086,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o match opt with | Some value -> Some value | None -> Some defValue - | _ -> - Helper.LibCall(com, "Option", "defaultArg", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | _ -> makeLibCall com r t i "Option" "defaultArg" args |> Some | "DefaultAsyncBuilder", _ -> makeImportLib com t "singleton" "AsyncBuilder" |> Some // Erased operators. // Rust compiles KeyValuePair as a struct tuple, but the KeyValue active pattern expects a regular tuple. @@ -1099,8 +1107,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | "ToString", _ -> toString com ctx r args |> Some | "CreateSequence", [ xs ] -> toSeq com t xs |> Some | ("CreateDictionary" | "CreateReadOnlyDictionary"), [ arg ] -> - Helper.LibCall(com, "HashMap", "new_from_tuple_array", t, [ toArray com t arg ]) - |> Some + libCall com r t "HashMap" "new_from_tuple_array" [ toArray com t arg ] |> Some | "CreateSet", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeSet com ctx r t args |> Some // Ranges | ("op_Range" | "op_RangeStep"), _ -> @@ -1116,8 +1123,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | Char -> "rangeChar", args | _ -> "rangeNumeric", addStep args - Helper.LibCall(com, "Range", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Range" meth args |> Some // Pipes and composition | "op_PipeRight", [ x; f ] | "op_PipeLeft", [ f; x ] -> curriedApply r t f [ x ] |> Some @@ -1131,9 +1137,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args | ("Failure" | "FailurePattern" | "LazyPattern" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args - | "Lock", _ -> - Helper.LibCall(com, "Monitor", "lock", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "Lock", _ -> libCallThis com r t i "Monitor" "lock" thisArg args |> Some | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions @@ -1156,12 +1160,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let argTypes = args |> List.map (fun a -> a.Type) match argTypes with - | Number(Decimal, _) :: _ -> - Helper.LibCall(com, "Decimal", "pown", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | Number(BigInt, _) :: _ -> - Helper.LibCall(com, "BigInt", "pow", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | Number(Decimal, _) :: _ -> libCallThis com r t i "Decimal" "pown" thisArg args |> Some + | Number(BigInt, _) :: _ -> libCallThis com r t i "BigInt" "pow" thisArg args |> Some | Number((Float32 | Float64), _) :: _ -> let meth = if i.CompiledName = "PowInteger" then @@ -1172,13 +1172,11 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o math r t args i.SignatureArgTypes meth |> Some | CustomOp com ctx r t "Pow" args e -> Some e | _ -> math r t args i.SignatureArgTypes "pow" |> Some - | ("Ceiling" | "Floor" as meth), _ -> + | ("Ceiling" | "Floor") as meth, _ -> let meth = Naming.lowerFirst meth match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" meth thisArg args |> Some | _ -> let meth = if meth = "ceiling" then @@ -1190,12 +1188,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | "Log", [ arg ] -> math r t args i.SignatureArgTypes "ln" |> Some | "Abs", _ -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "abs", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", "abs", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "abs" thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" "abs" thisArg args |> Some | _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some | ("Acos" | "Asin" | "Atan" | "Atan2" | "Cos" | "Cosh" | "Exp" | "Log" | "Log2" | "Log10" | "Sin" | "Sinh" | "Sqrt" | "Tan" | "Tanh"), _ -> @@ -1204,36 +1198,23 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> applyOp com ctx r t i.CompiledName args |> Some | "Round", _ -> match args with - | [ ExprType(Number(Decimal, _)) ] -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | [ ExprType(Number(Decimal, _)) ] -> libCallThis com r t i "Decimal" "round" thisArg args |> Some | [ ExprType(Number(Decimal, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Decimal", "roundTo", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | [ ExprType(Number(Decimal, _)); mode ] -> - Helper.LibCall(com, "Decimal", "roundMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ ExprType(Number(Decimal, _)); dp; mode ] -> - Helper.LibCall(com, "Decimal", "roundToMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + libCallThis com r t i "Decimal" "roundTo" thisArg args |> Some + | [ ExprType(Number(Decimal, _)); mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some + | [ ExprType(Number(Decimal, _)); dp; mode ] -> makeLibCall com r t i "Decimal" "roundToMode" args |> Some | [ ExprTypeAs(Number(Float64, _), arg) ] -> - //TODO: other midpoint modes for Double + // TODO: other midpoint modes for Double makeInstanceCall r t i arg "round" [] |> Some | _ -> None | "Truncate", [ arg ] -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "truncate", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "truncate" thisArg args |> Some | _ -> makeInstanceCall r t i arg "trunc" [] |> Some | "Sign", [ arg ] -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "sign", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", "sign", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" "sign" thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" "sign" thisArg args |> Some | ExprType(Number((Float16 | Float32 | Float64), _)) :: _ -> compare com ctx r arg (getZero com ctx arg.Type) |> Some | _ -> @@ -1241,12 +1222,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o TypeCast(sign, Int32.Number) |> Some | "DivRem", _ -> match args with - | [ x; y ] -> - Helper.LibCall(com, "Util", "divRem", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; y; rem ] -> - Helper.LibCall(com, "Util", "divRemOut", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ x; y ] -> makeLibCall com r t i "Util" "divRem" args |> Some + | [ x; y; rem ] -> makeLibCall com r t i "Util" "divRemOut" args |> Some | _ -> None // Numbers | "Infinity", _ -> makeGlobalIdent ("f64", "INFINITY", t) |> Some @@ -1266,9 +1243,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let v = sub (getRefCell com r t arg) (getOne com ctx t) setRefCell com r arg v |> Some // Concatenates two lists - | "op_Append", _ -> - Helper.LibCall(com, "List", "append", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "op_Append", _ -> libCallThis com r t i "List" "append" thisArg args |> Some | "IsNull", [ arg ] -> nullCheck r true arg |> Some | "Hash", [ arg ] -> getHashCode com ctx r arg |> Some // Comparison @@ -1276,26 +1251,20 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o applyCompareOp com ctx r t i.CompiledName left right |> Some | "Compare", [ left; right ] -> compare com ctx r left right |> Some | "Clamp", _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some - | ("Min" | "Max" as meth), _ -> + | ("Min" | "Max") as meth, _ -> match args.Head.Type with | Boolean | Char | String | Number _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some - | _ -> Helper.LibCall(com, "Native", Naming.lowerFirst meth, t, args, ?loc = r) |> Some - | ("MinMagnitude" | "MaxMagnitude" as meth), _ -> + | _ -> libCall com r t "Native" (Naming.lowerFirst meth) args |> Some + | ("MinMagnitude" | "MaxMagnitude") as meth, _ -> let meth = Naming.lowerFirst meth match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number(BigInt, _)) :: _ -> - Helper.LibCall(com, "BigInt", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | ExprType(Number _) :: _ -> - Helper.LibCall(com, "Numeric", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> libCallThis com r t i "Decimal" meth thisArg args |> Some + | ExprType(Number(BigInt, _)) :: _ -> libCallThis com r t i "BigInt" meth thisArg args |> Some + | ExprType(Number _) :: _ -> libCallThis com r t i "Numeric" meth thisArg args |> Some | _ -> None | "Not", [ operand ] -> // TODO: Check custom operator? makeUnOp r t operand UnaryNot |> Some @@ -1322,8 +1291,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt // Dynamic type of a boxed value: resolve via the runtime reflection // registry (populated by typeof). Returns the concrete type's info // when registered, else the value as an opaque placeholder. - Helper.LibCall(com, "Reflection", "getTypeFromObj", t, [ arg ], ?loc = r) - |> Some + libCall com r t "Reflection" "getTypeFromObj" [ arg ] |> Some else makeTypeInfo r arg.Type |> Some | _ -> None @@ -1346,42 +1314,32 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | _ -> meth match i.CompiledName, thisArg, args with - | ("IsBetween" as meth), None, _ -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsAscii" | "IsAsciiDigit" | "IsAsciiLetter" | "IsAsciiLetterLower" | "IsAsciiLetterUpper" | "IsAsciiLetterOrDigit" | "IsAsciiHexDigit" | "IsAsciiHexDigitLower" | "IsAsciiHexDigitUpper" as meth), + | ("IsBetween") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some + | ("IsAscii" | "IsAsciiDigit" | "IsAsciiLetter" | "IsAsciiLetterLower" | "IsAsciiLetterUpper" | "IsAsciiLetterOrDigit" | "IsAsciiHexDigit" | "IsAsciiHexDigitLower" | "IsAsciiHexDigitUpper") as meth, None, - [ c ] -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsUpper" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsWhiteSpace" as meth), + [ c ] -> makeLibCall com r t i "Char" meth args |> Some + | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsUpper" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsWhiteSpace") as meth, None, _ -> let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("GetNumericValue" | "GetUnicodeCategory" | "ConvertToUtf32" as meth), None, _ -> + makeLibCall com r t i "Char" meth args |> Some + | ("GetNumericValue" | "GetUnicodeCategory" | "ConvertToUtf32") as meth, None, _ -> let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Char" meth args |> Some | "ToString", None, [ ExprType(Char) ] -> toString com ctx r args |> Some | "ToString", Some c, [] -> toString com ctx r [ c ] |> Some - | ("ConvertFromUtf32" | "ToLower" | "ToLowerInvariant" | "ToUpper" | "ToUpperInvariant" as meth), None, [ c ] -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("TryParse" | "Parse" as meth), None, _ -> - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair" as meth), None, _ -> + | ("ConvertFromUtf32" | "ToLower" | "ToLowerInvariant" | "ToUpper" | "ToUpperInvariant") as meth, None, [ c ] -> + makeLibCall com r t i "Char" meth args |> Some + | ("TryParse" | "Parse") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some + | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair") as meth, None, _ -> $"Rust chars are Unicode scalar values, so surrogate tests will be false." |> addWarning com ctx.InlinePath r let meth = getMethodName meth args - Helper.LibCall(com, "Char", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Char" meth args |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _, _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -1393,20 +1351,20 @@ let getEnumerator com r t i (expr: Expr) = // | IsEntity (Types.regexMatchCollection) _ // | IsEntity (Types.regexGroupCollection) _ // | IsEntity (Types.regexCaptureCollection) _ - | Array _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ expr ], ?loc = r) - | List _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ expr ], ?loc = r) + | Array _ -> libCall com r t "Seq" "Enumerable::ofArray" [ expr ] + | List _ -> libCall com r t "Seq" "Enumerable::ofList" [ expr ] | String -> let en = toSeq com Any expr makeInstanceCall r t i en "GetEnumerator" [] | IsEntity (Types.hashset) _ | IsEntity (Types.iset) _ -> - let ar = Helper.LibCall(com, "HashSet", "entries", t, [ expr ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) + let ar = libCall com r t "HashSet" "entries" [ expr ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] | IsEntity (Types.dictionary) _ | IsEntity (Types.idictionary) _ | IsEntity (Types.ireadonlydictionary) _ -> - let ar = Helper.LibCall(com, "HashMap", "entries", t, [ expr ], [ expr.Type ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) + let ar = libCallTyped com r t "HashMap" "entries" [ expr ] [ expr.Type ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] | _ -> makeInstanceCall r t i expr "GetEnumerator" [] let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -1423,90 +1381,80 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt match i.CompiledName, thisArg, args with | ".ctor", _, _ -> match i.SignatureArgTypes with - | [ Char; Number(Int32, _) ] -> Helper.LibCall(com, "String", "fromChar", t, args, ?loc = r) |> Some - | [ MaybeNullable(Array(Char, _)) ] -> Helper.LibCall(com, "String", "fromChars", t, args, ?loc = r) |> Some + | [ Char; Number(Int32, _) ] -> libCall com r t "String" "fromChar" args |> Some + | [ MaybeNullable(Array(Char, _)) ] -> libCall com r t "String" "fromChars" args |> Some | [ MaybeNullable(Array(Char, _)); Number(Int32, _); Number(Int32, _) ] -> - Helper.LibCall(com, "String", "fromChars2", t, args, ?loc = r) |> Some + libCall com r t "String" "fromChars2" args |> Some | _ -> None - | "get_Length", Some c, _ -> Helper.LibCall(com, "String", "length", t, c :: args, ?loc = r) |> Some - | "get_Chars", Some c, _ -> Helper.LibCall(com, "String", "getCharAt", t, c :: args, ?loc = r) |> Some - | "EnumerateRunes", Some c, _ -> Helper.LibCall(com, "String", "enumerateRunes", t, [ c ], ?loc = r) |> Some + | "get_Length", Some c, _ -> libCall com r t "String" "length" (c :: args) |> Some + | "get_Chars", Some c, _ -> libCall com r t "String" "getCharAt" (c :: args) |> Some + | "EnumerateRunes", Some c, _ -> libCall com r t "String" "enumerateRunes" [ c ] |> Some | "CompareOrdinal", None, _ -> match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "compareOrdinal" args |> Some | [ ExprType String ExprType(Number(Int32, _)) ExprType String ExprType(Number(Int32, _)) - ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + ExprType(Number(Int32, _)) ] -> libCall com r t "String" "compareOrdinal2" args |> Some | _ -> None | "CompareTo", Some c, [ ExprTypeAs(String, arg) ] -> $"String.CompareTo will be compiled as String.CompareOrdinal" |> addWarning com ctx.InlinePath r - Helper.LibCall(com, "String", "compareOrdinal", t, [ c; arg ], ?loc = r) |> Some + libCall com r t "String" "compareOrdinal" [ c; arg ] |> Some | "Compare", None, _ -> $"String.Compare will be compiled as String.CompareOrdinal" |> addWarning com ctx.InlinePath r match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "compareOrdinal", t, args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "compareOrdinal" args |> Some | ExprType String :: ExprType String :: ExprType Boolean :: restArgs -> - Helper.LibCall(com, "String", "compareCase", t, args, ?loc = r) |> Some - | [ ExprType String; ExprType String; comparison ] -> - Helper.LibCall(com, "String", "compareWith", t, args, ?loc = r) |> Some + libCall com r t "String" "compareCase" args |> Some + | [ ExprType String; ExprType String; comparison ] -> libCall com r t "String" "compareWith" args |> Some | [ ExprType String ExprType(Number(Int32, _)) ExprType String ExprType(Number(Int32, _)) - ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "compareOrdinal2", t, args, ?loc = r) |> Some + ExprType(Number(Int32, _)) ] -> libCall com r t "String" "compareOrdinal2" args |> Some | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, _)) :: ExprType Boolean :: restArgs -> - Helper.LibCall(com, "String", "compareCase2", t, args, ?loc = r) |> Some + libCall com r t "String" "compareCase2" args |> Some | ExprType String :: ExprType(Number(Int32, _)) :: ExprType String :: ExprType(Number(Int32, _)) :: ExprType(Number(Int32, _)) :: comparison :: restArgs -> - Helper.LibCall(com, "String", "compareWith2", t, args, ?loc = r) |> Some + libCall com r t "String" "compareWith2" args |> Some | _ -> None | "Concat", None, _ -> match args with - | [ ExprTypeAs(IEnumerable, arg) ] -> - Helper.LibCall(com, "String", "concat", t, [ toArray com t arg ], ?loc = r) - |> Some + | [ ExprTypeAs(IEnumerable, arg) ] -> libCall com r t "String" "concat" [ toArray com t arg ] |> Some | [ ExprType String; ExprType String ] | [ ExprType String; ExprType String; ExprType String ] | [ ExprType String; ExprType String; ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "concat", t, [ makeArray String args ], ?loc = r) - |> Some - | [ ExprType(Array(String, _)) ] -> Helper.LibCall(com, "String", "concat", t, args, ?loc = r) |> Some + libCall com r t "String" "concat" [ makeArray String args ] |> Some + | [ ExprType(Array(String, _)) ] -> libCall com r t "String" "concat" args |> Some | _ -> None | "Contains", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "containsChar", t, c :: args, ?loc = r) |> Some - | [ ExprType Char; _comparison ] -> - Helper.LibCall(com, "String", "containsChar2", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "contains", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "contains2", t, c :: args, ?loc = r) |> Some + | [ ExprType Char ] -> libCall com r t "String" "containsChar" (c :: args) |> Some + | [ ExprType Char; _comparison ] -> libCall com r t "String" "containsChar2" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "contains" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "contains2" (c :: args) |> Some | _ -> None | "EndsWith", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "endsWithChar", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "endsWith", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> Helper.LibCall(com, "String", "endsWith2", t, c :: args, ?loc = r) |> Some - | [ pattern; ignoreCase; _culture ] -> - Helper.LibCall(com, "String", "endsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) - |> Some + | [ ExprType Char ] -> libCall com r t "String" "endsWithChar" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "endsWith" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "endsWith2" (c :: args) |> Some + | [ pattern; ignoreCase; _culture ] -> libCall com r t "String" "endsWith3" [ c; pattern; ignoreCase ] |> Some | _ -> None | "Equals", _, _ -> match thisArg, args with | Some x, [ ExprTypeAs(String, y) ] | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y) ] -> - Helper.LibCall(com, "String", "equalsOrdinal", t, [ x; y ], ?loc = r) |> Some + libCall com r t "String" "equalsOrdinal" [ x; y ] |> Some | Some x, [ ExprTypeAs(String, y); comparison ] | None, [ ExprTypeAs(String, x); ExprTypeAs(String, y); comparison ] -> - Helper.LibCall(com, "String", "equals2", t, [ x; y; comparison ], ?loc = r) - |> Some + libCall com r t "String" "equals2" [ x; y; comparison ] |> Some | _ -> None | "Format", None, _ -> match args with @@ -1536,18 +1484,18 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | Some suffix -> let methName = (Naming.lowerFirst i.CompiledName) + suffix - Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some + libCall com r t "String" methName (c :: args) |> Some | _ -> None - | "Insert", Some c, _ -> Helper.LibCall(com, "String", "insert", t, c :: args, ?loc = r) |> Some - | "IsNullOrEmpty", None, _ -> Helper.LibCall(com, "String", "isNullOrEmpty", t, args, ?loc = r) |> Some - | "IsNullOrWhiteSpace", None, _ -> Helper.LibCall(com, "String", "isNullOrWhitespace", t, args, ?loc = r) |> Some + | "Insert", Some c, _ -> libCall com r t "String" "insert" (c :: args) |> Some + | "IsNullOrEmpty", None, _ -> libCall com r t "String" "isNullOrEmpty" args |> Some + | "IsNullOrWhiteSpace", None, _ -> libCall com r t "String" "isNullOrWhitespace" args |> Some | "Join", None, _ -> let args = match args with | [ ExprTypeAs(String, sep); ExprTypeAs(IEnumerable, arg) ] -> [ sep; toArray com t arg ] | [ ExprTypeAs(String, sep); ExprTypeAs(Array(String, _), arg) ] -> [ sep; arg ] | [ ExprTypeAs(Char, sep); ExprTypeAs(Array(String, _), arg) ] -> - let sep = Helper.LibCall(com, "String", "ofChar", String, [ sep ]) + let sep = libCall com r String "String" "ofChar" [ sep ] [ sep; arg ] | [ ExprTypeAs(String, sep) @@ -1555,23 +1503,23 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt ExprTypeAs(Number(Int32, _), idx) ExprTypeAs(Number(Int32, _), cnt) ] -> let arg = - Helper.LibCall(com, "Array", "getSubArray", Array(String, MutableArray), [ arg; idx; cnt ]) + libCall com r (Array(String, MutableArray)) "Array" "getSubArray" [ arg; idx; cnt ] [ sep; arg ] | [ ExprTypeAs(Char, sep) ExprTypeAs(Array(String, _), arg) ExprTypeAs(Number(Int32, _), idx) ExprTypeAs(Number(Int32, _), cnt) ] -> - let sep = Helper.LibCall(com, "String", "ofChar", String, [ sep ]) + let sep = libCall com r String "String" "ofChar" [ sep ] let arg = - Helper.LibCall(com, "Array", "getSubArray", Array(String, MutableArray), [ arg; idx; cnt ]) + libCall com r (Array(String, MutableArray)) "Array" "getSubArray" [ arg; idx; cnt ] [ sep; arg ] | _ -> [] if not (List.isEmpty args) then - Helper.LibCall(com, "String", "join", t, args, ?loc = r) |> Some + libCall com r t "String" "join" args |> Some else None | ("PadLeft" | "PadRight"), Some c, _ -> @@ -1581,94 +1529,69 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | [ ExprTypeAs(Number(Int32, _), arg) ] -> let ch = makeTypeConst None Char ' ' - Helper.LibCall(com, "String", methName, t, [ c; arg; ch ], ?loc = r) |> Some - | [ ExprType(Number(Int32, _)); ExprType Char ] -> - Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some + libCall com r t "String" methName [ c; arg; ch ] |> Some + | [ ExprType(Number(Int32, _)); ExprType Char ] -> libCall com r t "String" methName (c :: args) |> Some | _ -> None | "Remove", Some c, _ -> match args with - | [ ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "remove", t, c :: args, ?loc = r) |> Some + | [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "String" "remove" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "remove2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "remove2" (c :: args) |> Some | _ -> None | "Replace", Some c, _ -> match args with - | [ ExprType String; ExprType String ] -> - Helper.LibCall(com, "String", "replace", t, c :: args, ?loc = r) |> Some - | [ ExprType Char; ExprType Char ] -> - Helper.LibCall(com, "String", "replaceChar", t, c :: args, ?loc = r) |> Some + | [ ExprType String; ExprType String ] -> libCall com r t "String" "replace" (c :: args) |> Some + | [ ExprType Char; ExprType Char ] -> libCall com r t "String" "replaceChar" (c :: args) |> Some | _ -> None | "Split", Some c, _ -> match args with | [] -> - Helper.LibCall(com, "String", "split", t, [ c; makeStrConst ""; makeIntConst -1; makeIntConst 0 ], ?loc = r) + libCall com r t "String" "split" [ c; makeStrConst ""; makeIntConst -1; makeIntConst 0 ] |> Some | [ ExprTypeAs(String, arg1) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some | [ ExprTypeAs(String, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) - |> Some + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(String, arg1) ExprTypeAs(Number(Int32, _), arg2) ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; arg2; arg3 ], ?loc = r) - |> Some + libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) - |> Some + libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(Array(String, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "splitStrings", t, [ c; arg1; arg2 ], ?loc = r) - |> Some + libCall com r t "String" "splitStrings" [ c; arg1; arg2 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ExprTypeAs(Number(Int32, _), arg2) ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "split", t, [ c; arg1; arg2; arg3 ], ?loc = r) - |> Some + libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ ExprTypeAs(Char, arg1) ] -> - Helper.LibCall( - com, - "String", - "splitChars", - t, - [ c; makeArray Char [ arg1 ]; makeIntConst -1; makeIntConst 0 ], - ?loc = r - ) + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; makeIntConst 0 ] |> Some | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall( - com, - "String", - "splitChars", - t, - [ c; makeArray Char [ arg1 ]; makeIntConst -1; arg2 ], - ?loc = r - ) + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; makeArray Char [ arg1 ]; arg2; arg3 ], ?loc = r) + libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; arg2; arg3 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; makeIntConst -1; makeIntConst 0 ], ?loc = r) + libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; makeIntConst -1; arg2 ], ?loc = r) - |> Some + libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(Int32, _), arg2) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; arg2; makeIntConst 0 ], ?loc = r) - |> Some + libCall com r t "String" "splitChars" [ c; arg1; arg2; makeIntConst 0 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ExprTypeAs(Number(Int32, _), arg2) ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> - Helper.LibCall(com, "String", "splitChars", t, [ c; arg1; arg2; arg3 ], ?loc = r) - |> Some + libCall com r t "String" "splitChars" [ c; arg1; arg2; arg3 ] |> Some // Remaining gap: the count-bearing string[] overload Split(string[], int, options) // for multi-element / non-literal arrays. splitStrings has no count parameter, and a @@ -1677,50 +1600,39 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | _ -> None | "StartsWith", Some c, _ -> match args with - | [ ExprType Char ] -> Helper.LibCall(com, "String", "startsWithChar", t, c :: args, ?loc = r) |> Some - | [ ExprType String ] -> Helper.LibCall(com, "String", "startsWith", t, c :: args, ?loc = r) |> Some - | [ ExprType String; _comparison ] -> - Helper.LibCall(com, "String", "startsWith2", t, c :: args, ?loc = r) |> Some - | [ pattern; ignoreCase; _culture ] -> - Helper.LibCall(com, "String", "startsWith3", t, [ c; pattern; ignoreCase ], ?loc = r) - |> Some + | [ ExprType Char ] -> libCall com r t "String" "startsWithChar" (c :: args) |> Some + | [ ExprType String ] -> libCall com r t "String" "startsWith" (c :: args) |> Some + | [ ExprType String; _comparison ] -> libCall com r t "String" "startsWith2" (c :: args) |> Some + | [ pattern; ignoreCase; _culture ] -> libCall com r t "String" "startsWith3" [ c; pattern; ignoreCase ] |> Some | _ -> None | "Substring", Some c, _ -> match args with - | [ ExprType(Number(Int32, _)) ] -> Helper.LibCall(com, "String", "substring", t, c :: args, ?loc = r) |> Some + | [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "String" "substring" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "substring2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "substring2" (c :: args) |> Some | _ -> None | "ToCharArray", Some c, _ -> match args with - | [] -> Helper.LibCall(com, "String", "toCharArray", t, c :: args, ?loc = r) |> Some + | [] -> makeLibCall com r t i "String" "toCharArray" (c :: args) |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "String", "toCharArray2", t, c :: args, ?loc = r) |> Some + makeLibCall com r t i "String" "toCharArray2" (c :: args) |> Some | _ -> None - | ("ToLower" | "ToLowerInvariant"), Some c, args -> - Helper.LibCall(com, "String", "toLower", t, c :: args, ?loc = r) |> Some - | ("ToUpper" | "ToUpperInvariant"), Some c, args -> - Helper.LibCall(com, "String", "toUpper", t, c :: args, ?loc = r) |> Some + | ("ToLower" | "ToLowerInvariant"), Some c, args -> libCall com r t "String" "toLower" (c :: args) |> Some + | ("ToUpper" | "ToUpperInvariant"), Some c, args -> libCall com r t "String" "toUpper" (c :: args) |> Some | ("Trim" | "TrimStart" | "TrimEnd"), Some c, _ -> let methName = Naming.lowerFirst i.CompiledName match args with - | [] -> Helper.LibCall(com, "String", methName, t, c :: args, ?loc = r) |> Some - | [ ExprType Char ] -> Helper.LibCall(com, "String", methName + "Char", t, c :: args, ?loc = r) |> Some - | [ ExprType(Array(Char, _)) ] -> - Helper.LibCall(com, "String", methName + "Chars", t, c :: args, ?loc = r) - |> Some + | [] -> libCall com r t "String" methName (c :: args) |> Some + | [ ExprType Char ] -> libCall com r t "String" (methName + "Char") (c :: args) |> Some + | [ ExprType(Array(Char, _)) ] -> libCall com r t "String" (methName + "Chars") (c :: args) |> Some | _ -> None | _ -> None let stringModule (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | "Concat", [ sep; arg ] -> - Helper.LibCall(com, "String", "join", t, [ sep; toArray com t arg ], ?loc = r) - |> Some - | meth, args -> - Helper.LibCall(com, "String", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Concat", [ sep; arg ] -> libCall com r t "String" "join" [ sep; toArray com t arg ] |> Some + | meth, args -> makeLibCall com r t i "String" (Naming.lowerFirst meth) args |> Some let stringBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with @@ -1729,14 +1641,14 @@ let stringBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Ex | (ExprType String :: _) -> let s = "sprintf!" |> emitFormat com None String args - Helper.LibCall(com, "Util", "sb_Append", t, [ sb; s ], ?loc = r) |> Some + libCall com r t "Util" "sb_Append" [ sb; s ] |> Some | (cultureInfo :: restArgs) -> $"StringBuilder.AppendFormat(): Format provider argument is ignored" |> addWarning com ctx.InlinePath r let s = "sprintf!" |> emitFormat com None String restArgs - Helper.LibCall(com, "Util", "sb_Append", t, [ sb; s ], ?loc = r) |> Some + libCall com r t "Util" "sb_Append" [ sb; s ] |> Some | _ -> None | _ -> bclType com ctx r t i thisArg args @@ -1761,7 +1673,7 @@ let formattableString let tag = if not hasFormat then - Helper.LibValue(com, "String", "fmt", Any) |> Some + libValue com r Any "String" "fmt" [] |> Some else let fmtArg = matches @@ -1769,7 +1681,7 @@ let formattableString |> Array.toList |> makeArray String - Helper.LibCall(com, "String", "fmtWith", Any, [ fmtArg ]) |> Some + libCall com r Any "String" "fmtWith" [ fmtArg ] |> Some let holes = matches @@ -1783,7 +1695,7 @@ let formattableString let template = makeStringTemplate tag str holes args |> makeValue r // Use a type cast to keep the FormattableString type TypeCast(template, t) |> Some - | "get_Format", Some x, _ -> Helper.LibCall(com, "String", "getFormat", t, [ x ], ?loc = r) |> Some + | "get_Format", Some x, _ -> libCall com r t "String" "getFormat" [ x ] |> Some | "get_ArgumentCount", Some x, _ -> getFieldWith r t (getField x "args") "length" |> Some | "GetArgument", Some x, [ idx ] -> getExpr r t (getField x "args") idx |> Some | "GetArguments", Some x, [] -> getFieldWith r t x "args" |> Some @@ -1792,58 +1704,45 @@ let formattableString let seqModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, args with // | "ToArray", [arg] -> - // Helper.LibCall(com, "Array", "ofSeq", t, args, i.SignatureArgTypes, ?loc=r) |> Some - | "ToList", [ arg ] -> - Helper.LibCall(com, "List", "ofSeq", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + // makeLibCall com r t i "Array" "ofSeq" args |> Some + | "ToList", [ arg ] -> makeLibCall com r t i "List" "ofSeq" args |> Some | "CreateEvent", [ addHandler; removeHandler; createHandler ] -> - Helper.LibCall(com, "Event", "createEvent", t, [ addHandler; removeHandler ], i.SignatureArgTypes, ?loc = r) + makeLibCall com r t i "Event" "createEvent" [ addHandler; removeHandler ] |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Seq", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Seq" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Seq", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + libCallThis com r t i "Seq" meth thisArg args |> Some let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ".ctor", _, [] -> Helper.LibCall(com, "NativeArray", "new_empty", t, args, ?loc = r) |> Some - | ".ctor", _, [ ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "NativeArray", "new_with_capacity", t, args, ?loc = r) - |> Some - | ".ctor", _, [ ExprType(IEnumerable) ] -> - Helper.LibCall(com, "NativeArray", "new_from_enumerable", t, args, ?loc = r) - |> Some + | ".ctor", _, [] -> libCall com r t "NativeArray" "new_empty" args |> Some + | ".ctor", _, [ ExprType(Number(Int32, _)) ] -> libCall com r t "NativeArray" "new_with_capacity" args |> Some + | ".ctor", _, [ ExprType(IEnumerable) ] -> libCall com r t "NativeArray" "new_from_enumerable" args |> Some | ".ctor", _, [ arg ] -> toArray com t arg |> Some - | "get_Capacity", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Capacity", t, [ ar ], ?loc = r) |> Some - | "get_Count", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "get_Capacity", Some ar, _ -> libCall com r t "NativeArray" "get_Capacity" [ ar ] |> Some + | "get_Count", Some ar, _ -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "get_Item", Some ar, [ idx ] -> getExpr r t ar idx |> Some | "set_Item", Some ar, [ idx; value ] -> setExpr r ar idx value |> Some - | "GetEnumerator", Some(MaybeCasted(ar)), _ -> - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + | "GetEnumerator", Some(MaybeCasted(ar)), _ -> libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | ("Add" | "AddRange" | "Clear" | "Contains" | "ConvertAll" | "Exists" | "GetRange" | "Slice" | "ForEach" | "FindAll" | "Find" | "FindLast" | "FindIndex" | "FindLastIndex" | "Insert" | "InsertRange" | "Remove" | "RemoveAt" | "RemoveAll" | "RemoveRange" | "ToArray" | "TrimExcess" | "TrueForAll") as meth, Some ar, args -> // methods without overrides let meth = Naming.lowerFirst meth - Helper.LibCall(com, "NativeArray", meth, t, (ar :: args), ?loc = r) |> Some + libCall com r t "NativeArray" meth (ar :: args) |> Some | ("BinarySearch" | "CopyTo" | "IndexOf" | "LastIndexOf" | "Reverse") as meth, Some ar, args -> // methods with some overrides let meth = meth |> toLowerFirstWithArgsCountSuffix (ar :: args) - Helper.LibCall(com, "NativeArray", meth, t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [] -> Helper.LibCall(com, "NativeArray", "sort", t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [ ExprType(DelegateType _) ] -> - Helper.LibCall(com, "NativeArray", "sortBy", t, (ar :: args), ?loc = r) |> Some - | "Sort", Some ar, [ comparer ] -> - Helper.LibCall(com, "NativeArray", "sortWith", t, (ar :: args), ?loc = r) - |> Some - | "Sort", Some ar, [ index; count; comparer ] -> - Helper.LibCall(com, "NativeArray", "sortWith2", t, (ar :: args), ?loc = r) - |> Some + libCall com r t "NativeArray" meth (ar :: args) |> Some + | "Sort", Some ar, [] -> libCall com r t "NativeArray" "sort" (ar :: args) |> Some + | "Sort", Some ar, [ ExprType(DelegateType _) ] -> libCall com r t "NativeArray" "sortBy" (ar :: args) |> Some + | "Sort", Some ar, [ comparer ] -> libCall com r t "NativeArray" "sortWith" (ar :: args) |> Some + | "Sort", Some ar, [ index; count; comparer ] -> libCall com r t "NativeArray" "sortWith2" (ar :: args) |> Some | _ -> None let collectionExtensions @@ -1856,12 +1755,8 @@ let collectionExtensions (args: Expr list) = match i.CompiledName, thisArg, args with - | "AddRange", None, [ ar; arg ] -> - Helper.LibCall(com, "Array", "addRangeInPlace", t, [ arg; ar ], ?loc = r) - |> Some - | "InsertRange", None, [ ar; idx; arg ] -> - Helper.LibCall(com, "Array", "insertRangeInPlace", t, [ idx; arg; ar ], ?loc = r) - |> Some + | "AddRange", None, [ ar; arg ] -> libCall com r t "Array" "addRangeInPlace" [ arg; ar ] |> Some + | "InsertRange", None, [ ar; idx; arg ] -> libCall com r t "Array" "insertRangeInPlace" [ idx; arg; ar ] |> Some | _ -> None let readOnlySpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -1908,50 +1803,40 @@ let createArray (com: ICompiler) ctx r t i count value = |> addErrorAndReturnNull com ctx.InlinePath r let copyToArray (com: ICompiler) r t (i: CallInfo) args = - Helper.LibCall(com, "Array", "copyTo", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" "copyTo" args |> Some let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "get_Length", Some ar, _ -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "get_Length", Some ar, _ -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "get_Item", Some ar, [ idx ] -> getExpr r t ar idx |> Some | "set_Item", Some ar, [ idx; value ] -> setExpr r ar idx value |> Some - | "Clone", Some ar, _ -> Helper.LibCall(com, "NativeArray", "toArray", t, [ ar ], ?loc = r) |> Some + | "Clone", Some ar, _ -> libCall com r t "NativeArray" "toArray" [ ar ] |> Some | "Copy", None, [ _source; _sourceIndex; _target; _targetIndex; _count ] -> copyToArray com r t i args | "Copy", None, [ source; target; count ] -> copyToArray com r t i [ source; makeIntConst 0; target; makeIntConst 0; count ] - | "GetEnumerator", Some ar, _ -> Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + | "GetEnumerator", Some ar, _ -> libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | ("ConvertAll" | "Exists" | "GetRange" | "ForEach" | "FindAll" | "Find" | "FindLast" | "FindIndex" | "FindLastIndex" | "TrueForAll") as meth, None, args -> // methods without overrides let meth = Naming.lowerFirst meth - Helper.LibCall(com, "NativeArray", meth, t, args, ?loc = r) |> Some + libCall com r t "NativeArray" meth args |> Some | ("BinarySearch" | "CopyTo" | "IndexOf" | "LastIndexOf" | "Reverse") as meth, None, args -> // methods with some overrides let meth = meth |> toLowerFirstWithArgsCountSuffix args - Helper.LibCall(com, "NativeArray", meth, t, args, ?loc = r) |> Some - | "Sort", None, [ ar ] -> Helper.LibCall(com, "NativeArray", "sort", t, args, ?loc = r) |> Some - | "Sort", None, [ ar; ExprType(DelegateType _) as comparer ] -> - Helper.LibCall(com, "NativeArray", "sortBy", t, args, ?loc = r) |> Some - | "Sort", None, [ ar; comparer ] -> Helper.LibCall(com, "NativeArray", "sortWith", t, args, ?loc = r) |> Some + libCall com r t "NativeArray" meth args |> Some + | "Sort", None, [ ar ] -> libCall com r t "NativeArray" "sort" args |> Some + | "Sort", None, [ ar; ExprType(DelegateType _) as comparer ] -> libCall com r t "NativeArray" "sortBy" args |> Some + | "Sort", None, [ ar; comparer ] -> libCall com r t "NativeArray" "sortWith" args |> Some | _ -> None let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | "ToSeq", [ ar ] -> - Helper.LibCall(com, "Seq", "ofArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfSeq", [ ar ] -> - Helper.LibCall(com, "Seq", "toArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfList", [ ar ] -> - Helper.LibCall(com, "List", "toArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ToList", args -> - Helper.LibCall(com, "List", "ofArray", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Length" | "Count"), [ ar ] -> Helper.LibCall(com, "NativeArray", "get_Count", t, [ ar ], ?loc = r) |> Some + | "ToSeq", [ ar ] -> makeLibCall com r t i "Seq" "ofArray" args |> Some + | "OfSeq", [ ar ] -> makeLibCall com r t i "Seq" "toArray" args |> Some + | "OfList", [ ar ] -> makeLibCall com r t i "List" "toArray" args |> Some + | "ToList", args -> makeLibCall com r t i "List" "ofArray" args |> Some + | ("Length" | "Count"), [ ar ] -> libCall com r t "NativeArray" "get_Count" [ ar ] |> Some | "Item", [ idx; ar ] -> getExpr r t ar idx |> Some | "Get", [ ar; idx ] -> getExpr r t ar idx |> Some | "Set", [ ar; idx; value ] -> setExpr r ar idx value |> Some @@ -1960,21 +1845,19 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex | "Empty", [] -> createArray com ctx r t i (makeIntConst 0) None |> Some | "Singleton", [ value ] -> createArray com ctx r t i (makeIntConst 1) (Some value) |> Some | "IsEmpty", [ ar ] -> makeInstanceCall r t i ar "is_empty" [] |> Some - | "Copy", [ ar ] -> Helper.LibCall(com, "NativeArray", "toArray", t, args, ?loc = r) |> Some + | "Copy", [ ar ] -> libCall com r t "NativeArray" "toArray" args |> Some | "CopyTo", args -> copyToArray com r t i args - | ("Concat" | "Transpose" as meth), [ arg ] -> - Helper.LibCall(com, "Array", Naming.lowerFirst meth, t, [ toArray com t arg ], i.SignatureArgTypes, ?loc = r) + | ("Concat" | "Transpose") as meth, [ arg ] -> + makeLibCall com r t i "Array" (Naming.lowerFirst meth) [ toArray com t arg ] |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "Array", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Array" meth args |> Some let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with @@ -1991,14 +1874,13 @@ let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Ex | [ ExprType Unit ] -> [ x ] | args -> args @ [ x ] - Helper.LibCall(com, "List", methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" methName args |> Some | "get_IsEmpty", Some c, _ -> Test(c, ListTest false, r) |> Some | "get_Empty", None, _ -> NewList(None, (genArg com ctx r 0 i.GenericArgs)) |> makeValue r |> Some | "Cons", None, [ h; t ] -> NewList(Some(h, t), (genArg com ctx r 0 i.GenericArgs)) |> makeValue r |> Some // | ("GetHashCode" | "Equals" | "CompareTo"), Some c, _ -> // makeInstanceCall r t i c i.CompiledName args |> Some - | "GetEnumerator", Some c, _ -> Helper.LibCall(com, "Seq", "Enumerable::ofList", t, [ c ], ?loc = r) |> Some + | "GetEnumerator", Some c, _ -> libCall com r t "Seq" "Enumerable::ofList" [ c ] |> Some | _ -> None let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -2011,25 +1893,19 @@ let listModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Exp |> Some // Use a cast to give it better chances of optimization (e.g. converting list // literals to arrays) after the beta reduction pass - | "ToSeq", [ arg ] -> - Helper.LibCall(com, "Seq", "ofList", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "OfSeq", [ arg ] -> - Helper.LibCall(com, "List", "ofSeq", t, args, i.SignatureArgTypes, ?loc = r) + | "ToSeq", [ arg ] -> makeLibCall com r t i "Seq" "ofList" args |> Some + | "OfSeq", [ arg ] -> makeLibCall com r t i "List" "ofSeq" args |> Some + | ("Concat" | "Transpose") as meth, [ arg ] -> + makeLibCall com r t i "List" (Naming.lowerFirst meth) [ toList com t arg ] |> Some - | ("Concat" | "Transpose" as meth), [ arg ] -> - Helper.LibCall(com, "List", Naming.lowerFirst meth, t, [ toList com t arg ], i.SignatureArgTypes, ?loc = r) - |> Some - | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> + | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy") as meth, args -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "List", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" meth args |> Some | meth, _ -> let meth = Naming.lowerFirst meth - Helper.LibCall(com, "List", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "List" meth args |> Some let discardUnitArgs args = match args with @@ -2048,43 +1924,36 @@ let sets (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Exp "IsProperSubsetOf", "isProperSubset" "IsProperSupersetOf", "isProperSuperset" "CopyTo", "copyToArray" ] meth, - Some callee -> Helper.LibCall(com, "Set", meth, t, callee :: args, ?loc = r) |> Some - | meth, _ -> + Some c -> libCall com r t "Set" meth (c :: args) |> Some + | meth, Some c -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - - let args = - match thisArg with - | Some callee -> args @ [ callee ] - | _ -> args - - Helper.LibCall(com, "Set", meth, t, args, ?loc = r) |> Some + libCall com r t "Set" meth (args @ [ c ]) |> Some + | meth, None -> + let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst + libCall com r t "Set" meth args |> Some let setModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let meth = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "Set", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Set" meth args |> Some let maps (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let args = discardUnitArgs args match i.CompiledName, thisArg with | ".ctor", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeMap com ctx r t args |> Some - | ReplaceName [ "CopyTo", "copyToArray" ] meth, Some callee -> - Helper.LibCall(com, "Map", meth, t, callee :: args, ?loc = r) |> Some - | meth, _ -> + | ReplaceName [ "CopyTo", "copyToArray" ] meth, Some c -> libCall com r t "Map" meth (c :: args) |> Some + | meth, Some c -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst - - let args = - match thisArg with - | Some callee -> args @ [ callee ] - | _ -> args - - Helper.LibCall(com, "Map", meth, t, args, ?loc = r) |> Some + libCall com r t "Map" meth (args @ [ c ]) |> Some + | meth, None -> + let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst + libCall com r t "Map" meth args |> Some let mapModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let meth = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "Map", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeLibCall com r t i "Map" meth args |> Some let results (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with @@ -2105,15 +1974,13 @@ let results (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr o | "ToArray" | "ToList" | "ToOption" - | "ToValueOption" as meth -> - Helper.LibCall(com, "Result", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "ToValueOption" as meth -> makeLibCall com r t i "Result" (Naming.lowerFirst meth) args |> Some | _ -> None let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | ".ctor", None -> NewOption(List.tryHead args, t.Generics.Head, false) |> makeValue r |> Some - | "get_Value", Some c -> Helper.LibCall(com, "Option", "getValue", t, [ c ], ?loc = r) |> Some + | "get_Value", Some c -> libCall com r t "Option" "getValue" [ c ] |> Some | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None @@ -2133,21 +2000,18 @@ let optionModule isStruct (com: ICompiler) (ctx: Context) r (t: Type) (i: CallIn | "GetValue", [ c ] -> Get(c, OptionValue, t, r) |> Some | "IsSome", [ c ] -> Test(c, OptionTest true, r) |> Some | "IsNone", [ c ] -> Test(c, OptionTest false, r) |> Some - | "OfObj", [ arg ] -> Helper.LibCall(com, "Native", "ofObj", t, args, ?loc = r) |> Some - | "ToObj", [ arg ] -> Helper.LibCall(com, "Native", "toObj", t, args, ?loc = r) |> Some - // | "ToArray", [ arg ] -> Helper.LibCall(com, "Array", "ofOption", t, args, ?loc = r) |> Some - // | "ToList", [ arg ] -> Helper.LibCall(com, "List", "ofOption", t, args, ?loc = r) |> Some - | meth, args -> - Helper.LibCall(com, "Option", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "OfObj", [ arg ] -> libCall com r t "Native" "ofObj" args |> Some + | "ToObj", [ arg ] -> libCall com r t "Native" "toObj" args |> Some + // | "ToArray", [ arg ] -> libCall com r t "Array" "ofOption" args |> Some + // | "ToList", [ arg ] -> libCall com r t "List" "ofOption" args |> Some + | meth, args -> makeLibCall com r t i "Option" (Naming.lowerFirst meth) args |> Some let parseBool (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, args with | ("Parse" | "TryParse" as method), args -> let meth = Naming.lowerFirst method + "Boolean" - Helper.LibCall(com, "Convert", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Convert" meth args |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -2172,16 +2036,16 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else [ str; makeIntConst style ] @ outValue - Helper.LibCall(com, moduleName, memberName, t, args, ?loc = r) + libCall com r t moduleName memberName args let isFloat = match i.SignatureArgTypes with | Number((Float16 | Float32 | Float64), _) :: _ -> true | _ -> false - match i.CompiledName, args with - | "IsNaN", [ arg ] when isFloat -> makeInstanceCall r t i arg "is_nan" [] |> Some - | "Log2", [ arg ] -> + match i.CompiledName, thisArg, args with + | "IsNaN", _, [ arg ] when isFloat -> makeInstanceCall r t i arg "is_nan" [] |> Some + | "Log2", _, [ arg ] -> let log = if isFloat then makeInstanceCall r t i arg "log2" [] @@ -2189,17 +2053,17 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op makeInstanceCall r UInt32.Number i arg "ilog2" [] TypeCast(log, t) |> Some - | "IsPositiveInfinity", [ arg ] when isFloat -> + | "IsPositiveInfinity", _, [ arg ] when isFloat -> let op1 = makeInstanceCall r t i arg "is_sign_positive" [] let op2 = makeInstanceCall r t i arg "is_infinite" [] Operation(Logical(LogicalAnd, op1, op2), Tags.empty, t, None) |> Some - | "IsNegativeInfinity", [ arg ] when isFloat -> + | "IsNegativeInfinity", _, [ arg ] when isFloat -> let op1 = makeInstanceCall r t i arg "is_sign_negative" [] let op2 = makeInstanceCall r t i arg "is_infinite" [] Operation(Logical(LogicalAnd, op1, op2), Tags.empty, t, None) |> Some - | "IsInfinity", [ arg ] when isFloat -> makeInstanceCall r t i arg "is_infinite" [] |> Some - | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _ -> operators com ctx r t i thisArg args - | ("Parse" | "TryParse") as meth, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> + | "IsInfinity", _, [ arg ] when isFloat -> makeInstanceCall r t i arg "is_infinite" [] |> Some + | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _, _ -> operators com ctx r t i thisArg args + | ("Parse" | "TryParse") as meth, _, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> let hexConst = int System.Globalization.NumberStyles.HexNumber let intConst = int System.Globalization.NumberStyles.Integer @@ -2219,7 +2083,7 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> addWarning com ctx.InlinePath r parseCall meth str args style |> Some - | ("Parse" | "TryParse") as meth, str :: _ -> + | ("Parse" | "TryParse") as meth, _, str :: _ -> let acceptedArgs = if meth = "Parse" then 1 @@ -2233,13 +2097,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let style = int System.Globalization.NumberStyles.Any parseCall meth str args style |> Some - | "Pow", (thisArg :: restArgs) -> makeInstanceCall r t i thisArg "powf" restArgs |> Some + | "Pow", _, (arg :: restArgs) -> makeInstanceCall r t i arg "powf" restArgs |> Some // | "ToString", [StringConst fmt] -> // let rustFmt = fmt // TODO: replace format specifiers with proper Rust format // let format = makeStrConst ("{0:" + rustFmt + "}") - // "sprintf!" |> emitFormat com r t [format; thisArg.Value] |> Some - | "ToString", _ -> toString com ctx r [ thisArg.Value ] |> Some - | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args + // "sprintf!" |> emitFormat com r t [format; thisArg] |> Some + | "ToString", Some c, _ -> toString com ctx r [ c ] |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None /// Drops culture and style arguments from a call. @@ -2258,7 +2122,7 @@ let dropCultureArgs (args: Expr list) = match e.Type with | DeclaredType(ent, _) -> match ent.FullName with - | "System.Globalization.CultureInfo" + | Types.cultureInfo | "System.IFormatProvider" -> true | _ -> false | Number(_, NumberInfo.IsEnum ent) -> @@ -2273,76 +2137,52 @@ let dropCultureArgs (args: Expr list) = let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = let args = dropCultureArgs args - match i.CompiledName, args with - | (".ctor" | "MakeDecimal"), ([ low; mid; high; isNegative; scale ] as args) -> - Helper.LibCall(com, "Decimal", "fromParts", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ".ctor", [ Value(NewArray(ArrayValues([ low; mid; high; signExp ] as args), _, _), _) ] -> - Helper.LibCall(com, "Decimal", "fromInts", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ".ctor", [ arg ] -> convertTo com ctx r t args |> Some - | "GetBits", _ -> - Helper.LibCall(com, "Decimal", "getBits", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Parse", _ -> - Helper.LibCall(com, "Decimal", "parse", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TryParse", _ -> - Helper.LibCall(com, "Decimal", "tryParse", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | Patterns.SetContains Operators.compareSet, [ left; right ] -> + match i.CompiledName, thisArg, args with + | (".ctor" | "MakeDecimal"), _, ([ low; mid; high; isNegative; scale ] as args) -> + makeLibCall com r t i "Decimal" "fromParts" args |> Some + | ".ctor", _, [ Value(NewArray(ArrayValues([ low; mid; high; signExp ] as args), _, _), _) ] -> + makeLibCall com r t i "Decimal" "fromInts" args |> Some + | ".ctor", _, [ arg ] -> convertTo com ctx r t args |> Some + | "GetBits", _, _ -> makeLibCall com r t i "Decimal" "getBits" args |> Some + | "Parse", _, _ -> makeLibCall com r t i "Decimal" "parse" args |> Some + | "TryParse", _, _ -> makeLibCall com r t i "Decimal" "tryParse" args |> Some + | Patterns.SetContains Operators.compareSet, _, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some - | Patterns.SetContains Operators.standardSet, _ -> applyOp com ctx r t i.CompiledName args |> Some - | "op_Explicit", _ -> convertTo com ctx r t args |> Some - | ("Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), + | Patterns.SetContains Operators.standardSet, _, _ -> applyOp com ctx r t i.CompiledName args |> Some + | "op_Explicit", _, _ -> convertTo com ctx r t args |> Some + | ("Abs" | "Sign" | "Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate") as meth, + _, _ -> let meth = Naming.lowerFirst meth - - Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _ -> - Helper.LibValue(com, "Decimal", Naming.removeGetSetPrefix i.CompiledName, t) - |> Some - | ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _ -> - Helper.LibCall(com, "Decimal", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "get_Scale", [] -> - match thisArg with - | Some c -> - Helper.LibCall(com, "Decimal", "scale", t, [ c ], i.SignatureArgTypes, ?loc = r) - |> Some - | None -> None - | "Round", _ -> + makeLibCall com r t i "Decimal" meth args |> Some + | ("CopySign" | "FromOACurrency" | "GetTypeCode" | "ToOACurrency") as meth, _, _ -> + makeLibCall com r t i "Decimal" (Naming.lowerFirst meth) args |> Some + | ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _, _ -> + libValue com r t "Decimal" (Naming.removeGetSetPrefix i.CompiledName) [] |> Some + | ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _, _ -> + makeLibCall com r t i "Decimal" (Naming.lowerFirst i.CompiledName) args |> Some + | "get_Scale", Some c, [] -> makeLibCall com r t i "Decimal" "scale" [ c ] |> Some + | "get_Scale", None, [] -> None + | "Round", _, _ -> match args with - | [ x ] -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; ExprTypeAs(Number(Int32, _), dp) ] -> - Helper.LibCall(com, "Decimal", "roundTo", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; mode ] -> - Helper.LibCall(com, "Decimal", "roundMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ x; dp; mode ] -> - Helper.LibCall(com, "Decimal", "roundToMode", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ x ] -> makeLibCall com r t i "Decimal" "round" args |> Some + | [ x; ExprTypeAs(Number(Int32, _), dp) ] -> makeLibCall com r t i "Decimal" "roundTo" args |> Some + | [ x; mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some + | [ x; dp; mode ] -> makeLibCall com r t i "Decimal" "roundToMode" args |> Some | _ -> None // | "ToString", [StringConst fmt] -> // let rustFmt = fmt // TODO: replace format specifiers with proper Rust format // let format = makeStrConst ("{0:" + rustFmt + "}") - // "sprintf!" |> emitFormat com r t [format; thisArg.Value] |> Some - | "ToString", _ -> + // "sprintf!" |> emitFormat com r t [format; thisArg] |> Some + | "ToString", Some c, [] -> // For d.ToString(provider) the provider is the only argument, so dropping // it leaves nothing for toString's decimal parameter; the receiver takes - // its place. d.ToString() already arrives with the receiver in args. - let args = - match args, thisArg with - | [], Some this -> [ this ] - | _ -> args + // its place. + let args = [ c ] - Helper.LibCall(com, "Decimal", "toString", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args + makeLibCall com r t i "Decimal" "toString" args |> Some + | "ToString", _, _ -> makeLibCall com r t i "Decimal" "toString" args |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None let bigints (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2351,31 +2191,17 @@ let bigints (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: | Patterns.SetContains Operators.compareSet, _, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some | Patterns.SetContains Operators.standardSet, _, _ -> applyOp com ctx r t i.CompiledName args |> Some - | "DivRem", None, [ x; y ] -> - Helper.LibCall(com, "BigInt", "divRem", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "DivRem", None, [ x; y; rem ] -> - Helper.LibCall(com, "BigInt", "divRemOut", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "DivRem", None, [ x; y ] -> makeLibCall com r t i "BigInt" "divRem" args |> Some + | "DivRem", None, [ x; y; rem ] -> makeLibCall com r t i "BigInt" "divRemOut" args |> Some | "op_Explicit", None, _ -> convertTo com ctx r t args |> Some - | "Log", None, [ arg1; arg2 ] -> - Helper.LibCall(com, "BigInt", "log", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | "Log", None, [ arg ] -> - Helper.LibCall(com, "BigInt", "ln", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | "Log2", None, [ arg ] -> - Helper.LibCall(com, "BigInt", "ilog2", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | "Log", None, [ arg1; arg2 ] -> makeLibCall com r t i "BigInt" "log" args |> Some + | "Log", None, [ arg ] -> makeLibCall com r t i "BigInt" "ln" args |> Some + | "Log2", None, [ arg ] -> makeLibCall com r t i "BigInt" "ilog2" args |> Some | meth, None, _ when meth.StartsWith("get_", StringComparison.Ordinal) -> let meth = meth |> Naming.removeGetSetPrefix |> Naming.lowerFirst - Helper.LibCall(com, "BigInt", meth, t, []) |> Some - | meth, None, _ -> - Helper.LibCall(com, "BigInt", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some c, _ -> - Helper.LibCall(com, "BigInt", Naming.lowerFirst meth, t, c :: args, i.SignatureArgTypes, ?loc = r) - |> Some + libCall com r t "BigInt" meth [] |> Some + | meth, None, _ -> makeLibCall com r t i "BigInt" (Naming.lowerFirst meth) args |> Some + | meth, Some c, _ -> makeLibCall com r t i "BigInt" (Naming.lowerFirst meth) (c :: args) |> Some // Compile static strings to their constant values // reference: https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/languageprimitives.errorstrings-module-%5bfsharp%5d @@ -2400,7 +2226,7 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr else applyOp com ctx r t operation args |> Some | "DivideByInt", _ -> applyOp com ctx r t i.CompiledName args |> Some - | "GenericZero", _ -> Helper.LibCall(com, "Native", "defaultOf", t, []) |> Some + | "GenericZero", _ -> libCall com r t "Native" "defaultOf" [] |> Some // | "GenericZero", _ -> getZero com ctx t |> Some | "GenericOne", _ -> getOne com ctx t |> Some | ("SByteWithMeasure" | "Int16WithMeasure" | "Int32WithMeasure" | "Int64WithMeasure" | "Float32WithMeasure" | "FloatWithMeasure" | "DecimalWithMeasure"), @@ -2442,18 +2268,12 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "UnboxFast", _, [ arg ] -> Some arg | "UnboxGeneric", _, [ arg ] -> TypeCast(arg, t) |> Some | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args - | "GetString", _, [ ar; idx ] -> Helper.LibCall(com, "String", "getCharAt", t, args, ?loc = r) |> Some - | "GetStringSlice", None, [ ar; lower; upper ] -> - Helper.LibCall(com, "String", "getSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetString", _, [ ar; idx ] -> libCall com r t "String" "getCharAt" args |> Some + | "GetStringSlice", None, [ ar; lower; upper ] -> makeLibCall com r t i "String" "getSlice" args |> Some | "GetArray", _, [ ar; idx ] -> getExpr r t ar idx |> Some | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some - | "GetArraySlice", None, [ ar; lower; upper ] -> - Helper.LibCall(com, "Array", "getSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "SetArraySlice", None, args -> - Helper.LibCall(com, "Array", "setSlice", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetArraySlice", None, [ ar; lower; upper ] -> makeLibCall com r t i "Array" "getSlice" args |> Some + | "SetArraySlice", None, args -> makeLibCall com r t i "Array" "setSlice" args |> Some | ("TypeTestGeneric" | "TypeTestFast"), None, [ expr ] -> Test(expr, TypeTest((genArg com ctx r 0 i.GenericArgs)), r) |> Some // | "CreateInstance", None, _ -> @@ -2467,23 +2287,17 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr // Type: PowDouble : float -> int -> float // Usage: PowDouble x n | "PowDouble", None, (thisArg :: restArgs) -> makeInstanceCall r t i thisArg "powf" restArgs |> Some - | "PowDecimal", None, _ -> - Helper.LibCall(com, "Decimal", "pown", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "PowDecimal", None, _ -> makeLibCall com r t i "Decimal" "pown" args |> Some // reference: https://msdn.microsoft.com/visualfsharpdocs/conceptual/operatorintrinsics.rangechar-function-%5bfsharp%5d // Type: RangeChar : char -> char -> char seq // Usage: RangeChar start stop - | "RangeChar", None, _ -> - Helper.LibCall(com, "Range", "rangeChar", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "RangeChar", None, _ -> makeLibCall com r t i "Range" "rangeChar" args |> Some // reference: https://msdn.microsoft.com/visualfsharpdocs/conceptual/operatorintrinsics.rangedouble-function-%5bfsharp%5d // Type: RangeDouble: float -> float -> float -> float seq // Usage: RangeDouble start step stop | ("RangeSByte" | "RangeByte" | "RangeInt16" | "RangeUInt16" | "RangeInt32" | "RangeUInt32" | "RangeInt64" | "RangeUInt64" | "RangeSingle" | "RangeDouble"), None, - args -> - Helper.LibCall(com, "Range", "rangeNumeric", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + args -> makeLibCall com r t i "Range" "rangeNumeric" args |> Some | _ -> None let runtimeHelpers (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = @@ -2505,7 +2319,7 @@ let funcs (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = match args, t with | [ arg ], DeclaredType(_, genArgs) -> uncurryExprAtRuntime com (List.length genArgs - 1) arg |> Some | _ -> emitExpr r t args "$0" |> Some - | "Invoke", Some callee -> Helper.Application(callee, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "Invoke", Some c -> Helper.Application(c, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None let keyValuePairs (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = @@ -2519,23 +2333,20 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp match i.CompiledName, thisArg with | ".ctor", None -> match i.SignatureArgTypes with - | [] -> Helper.LibCall(com, "HashMap", "new_empty", t, args) |> Some - | [ Number _ ] -> Helper.LibCall(com, "HashMap", "new_with_capacity", t, args) |> Some - | [ IEqualityComparer ] -> Helper.LibCall(com, "HashMap", "new_with_comparer", t, args) |> Some - | [ Number _; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_with_capacity_comparer", t, args) |> Some - | [ IEnumerable ] -> Helper.LibCall(com, "HashMap", "new_from_enumerable", t, args) |> Some - | [ IEnumerable; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_from_enumerable_comparer", t, args) |> Some - | [ IDictionary ] -> Helper.LibCall(com, "HashMap", "new_from_dictionary", t, args) |> Some - | [ IDictionary; IEqualityComparer ] -> - Helper.LibCall(com, "HashMap", "new_from_dictionary_comparer", t, args) |> Some + | [] -> libCall com r t "HashMap" "new_empty" args |> Some + | [ Number _ ] -> libCall com r t "HashMap" "new_with_capacity" args |> Some + | [ IEqualityComparer ] -> libCall com r t "HashMap" "new_with_comparer" args |> Some + | [ Number _; IEqualityComparer ] -> libCall com r t "HashMap" "new_with_capacity_comparer" args |> Some + | [ IEnumerable ] -> libCall com r t "HashMap" "new_from_enumerable" args |> Some + | [ IEnumerable; IEqualityComparer ] -> libCall com r t "HashMap" "new_from_enumerable_comparer" args |> Some + | [ IDictionary ] -> libCall com r t "HashMap" "new_from_dictionary" args |> Some + | [ IDictionary; IEqualityComparer ] -> libCall com r t "HashMap" "new_from_dictionary_comparer" args |> Some | _ -> None | "GetEnumerator", Some c -> - let ar = Helper.LibCall(com, "HashMap", "entries", t, [ c ], [ c.Type ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some - | "get_Item", Some c -> makeLibModuleCall com r t i "HashMap" "get" thisArg args |> Some - | "set_Item", Some c -> makeLibModuleCall com r t i "HashMap" "set" thisArg args |> Some + let ar = libCall com r t "HashMap" "entries" [ c ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some + | "get_Item", Some c -> makeLibModuleCall com r t i "HashMap" "get" (Some c) args |> Some + | "set_Item", Some c -> makeLibModuleCall com r t i "HashMap" "set" (Some c) args |> Some | meth, _ -> let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst makeLibModuleCall com r t i "HashMap" meth thisArg args |> Some @@ -2544,18 +2355,16 @@ let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op match i.CompiledName, thisArg with | ".ctor", None -> match i.SignatureArgTypes with - | [] -> Helper.LibCall(com, "HashSet", "new_empty", t, args) |> Some - | [ Number _ ] -> Helper.LibCall(com, "HashSet", "new_with_capacity", t, args) |> Some - | [ IEqualityComparer ] -> Helper.LibCall(com, "HashSet", "new_with_comparer", t, args) |> Some - | [ Number _; IEqualityComparer ] -> - Helper.LibCall(com, "HashSet", "new_with_capacity_comparer", t, args) |> Some - | [ IEnumerable ] -> Helper.LibCall(com, "HashSet", "new_from_enumerable", t, args) |> Some - | [ IEnumerable; IEqualityComparer ] -> - Helper.LibCall(com, "HashSet", "new_from_enumerable_comparer", t, args) |> Some + | [] -> libCall com r t "HashSet" "new_empty" args |> Some + | [ Number _ ] -> libCall com r t "HashSet" "new_with_capacity" args |> Some + | [ IEqualityComparer ] -> libCall com r t "HashSet" "new_with_comparer" args |> Some + | [ Number _; IEqualityComparer ] -> libCall com r t "HashSet" "new_with_capacity_comparer" args |> Some + | [ IEnumerable ] -> libCall com r t "HashSet" "new_from_enumerable" args |> Some + | [ IEnumerable; IEqualityComparer ] -> libCall com r t "HashSet" "new_from_enumerable_comparer" args |> Some | _ -> None | "GetEnumerator", Some c -> - let ar = Helper.LibCall(com, "HashSet", "entries", t, [ c ]) - Helper.LibCall(com, "Seq", "Enumerable::ofArray", t, [ ar ], ?loc = r) |> Some + let ar = libCall com r t "HashSet" "entries" [ c ] + libCall com r t "Seq" "Enumerable::ofArray" [ ar ] |> Some | "CopyTo", Some c -> let meth = i.CompiledName |> toLowerFirstWithArgsCountSuffix (c :: args) makeLibModuleCall com r t i "HashSet" meth thisArg args |> Some @@ -2569,8 +2378,8 @@ let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let collections (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with - | Some callee -> - match callee.Type with + | Some c -> + match c.Type with | IsEntity (Types.keyCollection) _ | IsEntity (Types.valueCollection) _ | IsEntity (Types.icollectionGeneric) _ @@ -2603,9 +2412,9 @@ let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) let enums (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "HasFlag", Some this, [ arg ] -> + | "HasFlag", Some c, [ arg ] -> // x.HasFlags(y) => (int x) &&& (int y) <> 0 - makeBinOp r (Int32.Number) this arg BinaryAndBitwise + makeBinOp r (Int32.Number) c arg BinaryAndBitwise |> fun bitwise -> makeEqOp r bitwise (makeIntConst 0) BinaryUnequal |> Some | Patterns.DicContains (dict [ "Parse", "parseEnum" @@ -2625,7 +2434,7 @@ let enums (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio [ genArg com ctx r 0 i.GenericArgs |> makeTypeInfo None; value; refValue ] | _ -> args - Helper.LibCall(com, "Reflection", meth, t, args, ?loc = r) |> Some + libCall com r t "Reflection" meth args |> Some | _ -> None let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = @@ -2638,8 +2447,7 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option | Number(kind, _) -> "getBytes" + kind.ToString() | x -> FableError $"Unsupported type in BitConverter.GetBytes(): %A{x}" |> raise - let expr = - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) + let expr = makeLibCall com r t i "BitConverter" memberName args if com.Options.TypedArrays then expr |> Some @@ -2648,28 +2456,24 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option | "ToString" -> let memberName = "toString" + args.Length.ToString() - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "BitConverter" memberName args |> Some | _ -> let memberName = Naming.lowerFirst i.CompiledName - Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "BitConverter" memberName args |> Some let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with - | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" as meth), + | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64") as meth, [ ExprType(String); ExprType(Number(Int32, _)) ] -> toRadixInt com ctx r t i args |> Some | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), [ arg ] -> toRoundInt com ctx r t i args |> Some | ("ToSingle" | "ToDouble" | "ToDecimal"), [ arg ] -> convertTo com ctx r t args |> Some | "ToChar", [ arg ] -> convertTo com ctx r t args |> Some | "ToString", [ arg ] -> toString com ctx r args |> Some - | "ToString", [ arg; ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Convert", "toStringRadix", t, args, ?loc = r) |> Some + | "ToString", [ arg; ExprType(Number(Int32, _)) ] -> libCall com r t "Convert" "toStringRadix" args |> Some | ("ToHexString" | "ToHexStringLower" | "FromHexString" | "ToBase64String" | "FromBase64String"), [ arg ] -> - Helper.LibCall(com, "Convert", (Naming.lowerFirst i.CompiledName), t, args, ?loc = r) - |> Some + libCall com r t "Convert" (Naming.lowerFirst i.CompiledName) args |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2705,12 +2509,12 @@ let makeMemberCall com ctx r t i moduleName memberName (thisArg: Expr option) (a let args = ignoreFormatProvider i.CompiledName args match thisArg with - | Some callee -> makeInstanceCall r t i callee memberName args + | Some c -> makeInstanceCall r t i c memberName args | None -> makeStaticMemberCall com r t i moduleName memberName args let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [] -> "new_empty" |> Some | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some @@ -2769,21 +2573,20 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ] -> "new_ymdhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateTime" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | meth -> makeMemberCall com ctx r t i "DateTime" meth thisArg args |> Some + | "Subtract", None -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateTime" meth thisArg args |> Some let dateTimeOffsets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [] -> "new_empty" |> Some | ExprType(Number(Int64, _)) :: _ -> "new_ticks" |> Some @@ -2816,35 +2619,32 @@ let dateTimeOffsets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: _offset ] -> "new_ymdhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateTimeOffset" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | meth -> makeMemberCall com ctx r t i "DateTimeOffset" meth thisArg args |> Some + | "Subtract", None -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateTimeOffset" meth thisArg args |> Some let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_ymd" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateOnly" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "ToDateTime" when args.Length = 2 -> makeInstanceCall r t i thisArg.Value "toDateTime2" args |> Some - | meth -> makeMemberCall com ctx r t i "DateOnly" meth thisArg args |> Some + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "ToDateTime", Some c when args.Length = 2 -> makeInstanceCall r t i c "toDateTime2" args |> Some + | "ToDateTime", None when args.Length = 2 -> None + | meth, thisArg -> makeMemberCall com ctx r t i "DateOnly" meth thisArg args |> Some let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_hm" |> Some @@ -2860,80 +2660,63 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op ExprType(Number(Int32, _)) ] -> "new_hms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "TimeOnly" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args - | "Add" when args.Length = 2 -> makeInstanceCall r t i thisArg.Value "add2" args |> Some - | meth -> makeMemberCall com ctx r t i "TimeOnly" meth thisArg args |> Some - -let rune (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let libCall memberName callArgs = - Helper.LibCall(com, "Rune", memberName, t, callArgs, i.SignatureArgTypes, ?loc = r) - - let instanceCall memberName callArgs = - libCall memberName (thisArg.Value :: callArgs) - - let firstArg = - match args with - | arg :: _ -> [ arg ] - | [] -> [] - - let isCultureInfoArg (arg: Expr) = - match arg.Type with - | DeclaredType(ent, _) -> ent.FullName = "System.Globalization.CultureInfo" - | _ -> false + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args + | "Add", Some c when args.Length = 2 -> makeInstanceCall r t i c "add2" args |> Some + | "Add", None when args.Length = 2 -> None + | meth, thisArg -> makeMemberCall com ctx r t i "TimeOnly" meth thisArg args |> Some +let runes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", None, [ ExprType(Char) ] -> Some args.Head - | ".ctor", None, [ ExprType(Number(Int32, _)) ] -> libCall "newInt" args |> Some - | ".ctor", None, [ ExprType(Number(UInt32, _)) ] -> libCall "newUInt" args |> Some - | ".ctor", None, [ ExprType(Char); ExprType(Char) ] -> libCall "newPair" args |> Some - | ("ReplacementChar" | "get_ReplacementChar"), None, [] -> libCall "replacementChar" [] |> Some + | ".ctor", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "newInt" args |> Some + | ".ctor", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "newUInt" args |> Some + | ".ctor", None, [ ExprType(Char); ExprType(Char) ] -> makeLibCall com r t i "Rune" "newPair" args |> Some + | ("ReplacementChar" | "get_ReplacementChar"), None, [] -> makeLibCall com r t i "Rune" "replacementChar" [] |> Some | "op_Explicit", None, [ ExprType(Char) ] -> Some args.Head - | "op_Explicit", None, [ ExprType(Number(Int32, _)) ] -> libCall "newInt" args |> Some - | "op_Explicit", None, [ ExprType(Number(UInt32, _)) ] -> libCall "newUInt" args |> Some - | "TryCreate", None, [ ExprType(Char); _ ] -> libCall "tryCreateChar" args |> Some - | "TryCreate", None, [ ExprType(Char); ExprType(Char); _ ] -> libCall "tryCreatePair" args |> Some - | "TryCreate", None, [ ExprType(Number(Int32, _)); _ ] -> libCall "tryCreateInt" args |> Some - | "TryCreate", None, [ ExprType(Number(UInt32, _)); _ ] -> libCall "tryCreateUInt" args |> Some - | "get_Value", Some _, [] -> instanceCall "value" [] |> Some - | "get_Utf8SequenceLength", Some _, [] -> instanceCall "utf8SequenceLength" [] |> Some - | "get_Utf16SequenceLength", Some _, [] -> instanceCall "utf16SequenceLength" [] |> Some - | "get_IsAscii", Some _, [] -> instanceCall "isAscii" [] |> Some - | "get_IsBmp", Some _, [] -> instanceCall "isBmp" [] |> Some - | "get_Plane", Some _, [] -> instanceCall "plane" [] |> Some - | "GetNumericValue", None, _ -> libCall "getNumericValue" firstArg |> Some - | "GetUnicodeCategory", None, _ -> libCall "getUnicodeCategory" firstArg |> Some + | "op_Explicit", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "newInt" args |> Some + | "op_Explicit", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "newUInt" args |> Some + | "TryCreate", None, [ ExprType(Char); _ ] -> makeLibCall com r t i "Rune" "tryCreateChar" args |> Some + | "TryCreate", None, [ ExprType(Char); ExprType(Char); _ ] -> + makeLibCall com r t i "Rune" "tryCreatePair" args |> Some + | "TryCreate", None, [ ExprType(Number(Int32, _)); _ ] -> makeLibCall com r t i "Rune" "tryCreateInt" args |> Some + | "TryCreate", None, [ ExprType(Number(UInt32, _)); _ ] -> makeLibCall com r t i "Rune" "tryCreateUInt" args |> Some + | "get_Value", Some c, [] -> makeLibCall com r t i "Rune" "value" [ c ] |> Some + | "get_Utf8SequenceLength", Some c, [] -> makeLibCall com r t i "Rune" "utf8SequenceLength" [ c ] |> Some + | "get_Utf16SequenceLength", Some c, [] -> makeLibCall com r t i "Rune" "utf16SequenceLength" [ c ] |> Some + | "get_IsAscii", Some c, [] -> makeLibCall com r t i "Rune" "isAscii" [ c ] |> Some + | "get_IsBmp", Some c, [] -> makeLibCall com r t i "Rune" "isBmp" [ c ] |> Some + | "get_Plane", Some c, [] -> makeLibCall com r t i "Rune" "plane" [ c ] |> Some + | "GetNumericValue", None, [ arg ] -> makeLibCall com r t i "Rune" "getNumericValue" [ arg ] |> Some + | "GetUnicodeCategory", None, [ arg ] -> makeLibCall com r t i "Rune" "getUnicodeCategory" [ arg ] |> Some | ("IsControl" | "IsDigit" | "IsLetter" | "IsLetterOrDigit" | "IsLower" | "IsNumber" | "IsPunctuation" | "IsSeparator" | "IsSymbol" | "IsUpper" | "IsWhiteSpace"), None, - _ -> + [ arg ] -> let meth = Naming.lowerFirst i.CompiledName - libCall meth firstArg |> Some - | "IsValid", None, [ ExprType(Number(Int32, _)) ] -> libCall "isValidInt" args |> Some - | "IsValid", None, [ ExprType(Number(UInt32, _)) ] -> libCall "isValidUInt" args |> Some - | "ToLower", None, [ runeArg; cultureInfo ] when isCultureInfoArg cultureInfo -> - libCall "toLowerInvariant" [ runeArg ] |> Some - | "ToLowerInvariant", None, _ -> libCall "toLowerInvariant" firstArg |> Some - | "ToUpper", None, [ runeArg; cultureInfo ] when isCultureInfoArg cultureInfo -> - libCall "toUpperInvariant" [ runeArg ] |> Some - | "ToUpperInvariant", None, _ -> libCall "toUpperInvariant" firstArg |> Some - | "GetRuneAt", None, _ -> libCall "getRuneAt" args |> Some - | "TryGetRuneAt", None, _ -> libCall "tryGetRuneAt" args |> Some - | "Parse", None, _ -> libCall "parse" firstArg |> Some - | "TryParse", None, _ -> libCall "tryParse" args |> Some - | "ToString", Some _, _ -> instanceCall "toString" [] |> Some - | "CompareTo", Some _, _ -> instanceCall "compareTo" args |> Some - | "Compare", None, _ -> libCall "compareTo" args |> Some - | "Equals", Some _, _ -> instanceCall "equals" args |> Some - | "Equals", None, _ -> libCall "equals" args |> Some - | "GetHashCode", Some _, _ -> libCall "getHashCode" [ thisArg.Value ] |> Some + makeLibCall com r t i "Rune" meth args |> Some + | "IsValid", None, [ ExprType(Number(Int32, _)) ] -> makeLibCall com r t i "Rune" "isValidInt" args |> Some + | "IsValid", None, [ ExprType(Number(UInt32, _)) ] -> makeLibCall com r t i "Rune" "isValidUInt" args |> Some + | "ToLower", None, [ runeArg; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Rune" "toLowerInvariant" [ runeArg ] |> Some + | "ToLowerInvariant", None, [ arg ] -> makeLibCall com r t i "Rune" "toLowerInvariant" [ arg ] |> Some + | "ToUpper", None, [ runeArg; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Rune" "toUpperInvariant" [ runeArg ] |> Some + | "ToUpperInvariant", None, [ arg ] -> makeLibCall com r t i "Rune" "toUpperInvariant" [ arg ] |> Some + | "GetRuneAt", None, _ -> makeLibCall com r t i "Rune" "getRuneAt" args |> Some + | "TryGetRuneAt", None, _ -> makeLibCall com r t i "Rune" "tryGetRuneAt" args |> Some + | "Parse", None, [ arg ] -> makeLibCall com r t i "Rune" "parse" args |> Some + | "TryParse", None, _ -> makeLibCall com r t i "Rune" "tryParse" args |> Some + | "ToString", Some c, _ -> makeLibCall com r t i "Rune" "toString" [ c ] |> Some + | "CompareTo", Some c, _ -> makeLibCall com r t i "Rune" "compareTo" (c :: args) |> Some + | "Compare", None, _ -> makeLibCall com r t i "Rune" "compareTo" args |> Some + | "Equals", Some c, _ -> makeLibCall com r t i "Rune" "equals" (c :: args) |> Some + | "Equals", None, _ -> makeLibCall com r t i "Rune" "equals" args |> Some + | "GetHashCode", Some c, _ -> makeLibCall com r t i "Rune" "getHashCode" [ c ] |> Some | _ -> None let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = // let callee = match i.callee with Some c -> c | None -> i.args.Head - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with | [ ExprType(Number(Int64, _)) ] -> "new_ticks" |> Some | [ ExprType(Number(Int32, _)); ExprType(Number(Int32, _)); ExprType(Number(Int32, _)) ] -> "new_hms" |> Some @@ -2954,29 +2737,25 @@ let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ] -> "new_dhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "TimeSpan" meth args) - | "Compare" - | "CompareTo" - | "Equals" - | "GetHashCode" -> valueTypes com ctx r t i thisArg args + | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args // | "Zero" etc. -> // for static fields, see tryField - | "Add" -> - Operation(Binary(BinaryOperator.BinaryPlus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", Some c -> + Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) |> Some - | "Subtract" -> - Operation(Binary(BinaryOperator.BinaryMinus, thisArg.Value, args.Head), Tags.empty, t, r) + | "Add", None -> None + | "Subtract", Some c -> + Operation(Binary(BinaryOperator.BinaryMinus, c, args.Head), Tags.empty, t, r) |> Some - | "Multiply" -> - Operation(Binary(BinaryOperator.BinaryMultiply, thisArg.Value, args.Head), Tags.empty, t, r) + | "Subtract", None -> None + | "Multiply", Some c -> + Operation(Binary(BinaryOperator.BinaryMultiply, c, args.Head), Tags.empty, t, r) |> Some - | "Divide" -> - Operation(Binary(BinaryOperator.BinaryDivide, thisArg.Value, args.Head), Tags.empty, t, r) + | "Multiply", None -> None + | "Divide", Some c -> + Operation(Binary(BinaryOperator.BinaryDivide, c, args.Head), Tags.empty, t, r) |> Some - | "FromDays" - | "FromHours" - | "FromMinutes" - | "FromSeconds" - | "FromMilliseconds" - | "FromMicroseconds" as meth -> + | ("FromDays" | "FromHours" | "FromMinutes" | "FromSeconds" | "FromMilliseconds" | "FromMicroseconds") as meth, + thisArg -> match args with | [ ExprType(Number(Float64, _)) ] -> // overloads that take a float @@ -2986,16 +2765,14 @@ let timeSpans (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let argCount = List.length args let meth = meth + (string argCount) makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some - | meth -> makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some + | meth, thisArg -> makeMemberCall com ctx r t i "TimeSpan" meth thisArg args |> Some let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ".ctor", _, _ -> - Helper.LibCall(com, "Timer", "default", t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) - |> Some + | ".ctor", _, _ -> libCallCons com r t i "Timer" "default" args |> Some | Naming.StartsWith "get_" meth, Some x, _ -> getFieldWith r t x meth |> Some | Naming.StartsWith "set_" meth, Some x, [ value ] -> setExpr r x (makeStrConst meth) value |> Some - | meth, Some callee, args -> makeInstanceCall r t i callee meth args |> Some + | meth, Some c, args -> makeInstanceCall r t i c meth args |> Some | _ -> None let systemEnv @@ -3009,58 +2786,32 @@ let systemEnv = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") - | "GetEnvironmentVariable" -> - Helper.LibCall(com, "Environment", "getEnvironmentVariable", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "get_CurrentDirectory" -> - Helper.LibCall(com, "Environment", "getCurrentDirectory", t, [], ?loc = r) - |> Some + | "GetEnvironmentVariable" -> makeLibCall com r t i "Environment" "getEnvironmentVariable" args |> Some + | "get_CurrentDirectory" -> libCall com r t "Environment" "getCurrentDirectory" [] |> Some | _ -> None let paths (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "Combine" -> match args with - | [ _; _ ] -> - Helper.LibCall(com, "Path", "combine2", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ _; _; _ ] -> - Helper.LibCall(com, "Path", "combine3", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ _; _ ] -> makeLibCall com r t i "Path" "combine2" args |> Some + | [ _; _; _ ] -> makeLibCall com r t i "Path" "combine3" args |> Some | _ -> None - | "GetDirectoryName" -> - Helper.LibCall(com, "Path", "getDirectoryName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileName" -> - Helper.LibCall(com, "Path", "getFileName", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetFileNameWithoutExtension" -> - Helper.LibCall(com, "Path", "getFileNameWithoutExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetExtension" -> - Helper.LibCall(com, "Path", "getExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "HasExtension" -> - Helper.LibCall(com, "Path", "hasExtension", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "GetTempPath" -> Helper.LibCall(com, "Path", "getTempPath", t, [], ?loc = r) |> Some - | "GetRandomFileName" -> Helper.LibCall(com, "Path", "getRandomFileName", t, [], ?loc = r) |> Some + | "GetDirectoryName" -> makeLibCall com r t i "Path" "getDirectoryName" args |> Some + | "GetFileName" -> makeLibCall com r t i "Path" "getFileName" args |> Some + | "GetFileNameWithoutExtension" -> makeLibCall com r t i "Path" "getFileNameWithoutExtension" args |> Some + | "GetExtension" -> makeLibCall com r t i "Path" "getExtension" args |> Some + | "HasExtension" -> makeLibCall com r t i "Path" "hasExtension" args |> Some + | "GetTempPath" -> libCall com r t "Path" "getTempPath" [] |> Some + | "GetRandomFileName" -> libCall com r t "Path" "getRandomFileName" [] |> Some | _ -> None let files (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with - | "Exists" -> - Helper.LibCall(com, "File", "exists", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "WriteAllText" -> - Helper.LibCall(com, "File", "writeAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ReadAllText" -> - Helper.LibCall(com, "File", "readAllText", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Delete" -> - Helper.LibCall(com, "File", "delete", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Exists" -> makeLibCall com r t i "File" "exists" args |> Some + | "WriteAllText" -> makeLibCall com r t i "File" "writeAllText" args |> Some + | "ReadAllText" -> makeLibCall com r t i "File" "readAllText" args |> Some + | "Delete" -> makeLibCall com r t i "File" "delete" args |> Some | _ -> None // Initial support, making at least InvariantCulture compile-able @@ -3085,65 +2836,57 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti match i.CompiledName, thisArg with | ".ctor", _ -> match args with - | [] -> Helper.LibCall(com, "Random", "new", t, [], [], ?loc = r) |> Some - | args -> - Helper.LibCall(com, "Random", "new_seeded", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some thisArg -> + | [] -> libCall com r t "Random" "new" [] |> Some + | args -> makeLibCall com r t i "Random" "new_seeded" args |> Some + | meth, Some c -> let meth = if meth = "Next" then $"next{List.length args}" else meth |> Naming.lowerFirst - Helper.InstanceCall(thisArg, meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + Helper.InstanceCall(c, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | "get_None" // TODO: implement as non-cancellable token - | ".ctor" -> - Helper.LibCall(com, "Async", "createCancellationToken", t, args, i.SignatureArgTypes) - |> Some - | "get_Token" -> thisArg - | "Cancel" - | "CancelAfter" - | "get_IsCancellationRequested" - | "ThrowIfCancellationRequested" -> + match i.CompiledName, thisArg with + // TODO: implement get_None as a non-cancellable token + | ("get_None" | ".ctor"), _ -> makeLibCall com r t i "Async" "createCancellationToken" args |> Some + | "get_Token", thisArg -> thisArg + | ("Cancel" | "CancelAfter" | "get_IsCancellationRequested" | "ThrowIfCancellationRequested"), thisArg -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst makeLibModuleCall com r t i "Async" meth thisArg args |> Some // TODO: Add check so CancellationTokenSource cannot be cancelled after disposed? - | "Dispose" -> Null Type.Unit |> makeValue r |> Some - | "Register" -> makeInstanceCall r t i thisArg.Value "register" args |> Some + | "Dispose", _ -> Null Type.Unit |> makeValue r |> Some + | "Register", Some c -> makeInstanceCall r t i c "register" args |> Some | _ -> None let monitor (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with - | "Enter" -> Helper.LibCall(com, "Monitor", "enter", t, args, ?loc = r) |> Some - | "Exit" -> Helper.LibCall(com, "Monitor", "exit", t, args, ?loc = r) |> Some + | "Enter" -> libCall com r t "Monitor" "enter" args |> Some + | "Exit" -> libCall com r t "Monitor" "exit" args |> Some | _ -> None let tasks com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, i.GenericArgs with - | ".ctor", None, [ tType ] -> Helper.LibCall(com, "Task", "new", tType, args, ?loc = r) |> Some - | "FromResult", None, [ tType ] -> Helper.LibCall(com, "Task", "from_result", tType, args, ?loc = r) |> Some - | "get_Result", Some callee, _ -> makeInstanceCall r t i callee "get_result" args |> Some + | ".ctor", None, [ tType ] -> libCall com r tType "Task" "new" args |> Some + | "FromResult", None, [ tType ] -> libCall com r tType "Task" "from_result" args |> Some + | "get_Result", Some c, _ -> makeInstanceCall r t i c "get_result" args |> Some | _ -> None let threads com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, i.GenericArgs, args with - | ".ctor", None, [], _ -> Helper.LibCall(com, "Thread", "new", t, args, ?loc = r) |> Some - | "Sleep", None, _, [ ExprType(Number(Int32, _)) ] -> - Helper.LibCall(com, "Thread", "sleep", t, args, ?loc = r) |> Some - | "Start", Some callee, [], [] -> makeInstanceCall r t i callee "start" args |> Some - | "Join", Some callee, [], [] -> makeInstanceCall r t i callee "join" args |> Some + | ".ctor", None, [], _ -> libCall com r t "Thread" "new" args |> Some + | "Sleep", None, _, [ ExprType(Number(Int32, _)) ] -> libCall com r t "Thread" "sleep" args |> Some + | "Start", Some c, [], [] -> makeInstanceCall r t i c "start" args |> Some + | "Join", Some c, [], [] -> makeInstanceCall r t i c "join" args |> Some | _ -> None let activator (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | "CreateInstance", None, ([ _type ] | [ _type; (ExprType(Array(Any, _))) ]) -> - Helper.LibCall(com, "Reflection", "createInstance", t, args, ?loc = r) |> Some + | "CreateInstance", None, [ _type ] + | "CreateInstance", None, [ _type; (ExprType(Array(Any, _))) ] -> + libCall com r t "Reflection" "createInstance" args |> Some | _ -> None // alternative member suffix for languages that don't support member overloads @@ -3187,7 +2930,7 @@ let bclNativeImpl com ctx r t i moduleName memberName (thisArg: Expr option) (ar let memberName = memberName + suffix match thisArg with - | Some callee -> makeInstanceCall r t i callee memberName args + | Some c -> makeInstanceCall r t i c memberName args | None -> makeStaticMemberCall com r t i moduleName memberName args let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3205,10 +2948,8 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - | ("get_Unicode" | "get_UTF8"), _, _ -> - Helper.LibCall(com, "Encoding", i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("GetBytes" | "GetByteCount"), Some callee, ExprType(Array(Char, _)) :: _ -> + | ("get_Unicode" | "get_UTF8"), _, _ -> makeLibCall com r t i "Encoding" i.CompiledName args |> Some + | ("GetBytes" | "GetByteCount"), Some c, ExprType(Array(Char, _)) :: _ -> let meth = Naming.lowerFirst i.CompiledName + "FromChars" let meth = @@ -3217,9 +2958,9 @@ let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else meth - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | ("GetBytes" | "GetByteCount" | "GetChars" | "GetCharCount" | "GetMaxByteCount" | "GetMaxCharCount" | "GetString"), - Some callee, + Some c, _ -> let meth = Naming.lowerFirst i.CompiledName @@ -3229,24 +2970,24 @@ let encoding (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else meth - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let enumerators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with - | meth, Some callee -> + | meth, Some c -> // // Enumerators are mangled, use the fully qualified name // let isGenericCurrent = i.CompiledName = "get_Current" && i.DeclaringEntityFullName <> Types.ienumerator // let entityName = if isGenericCurrent then Types.ienumeratorGeneric else Types.ienumerator // let methName = entityName + "." + i.CompiledName - makeInstanceCall r t i callee meth args |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let enumerables (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (_: Expr list) = match i.CompiledName, thisArg with // This property only belongs to Key and Value Collections - | "get_Count", Some callee -> Helper.LibCall(com, "Seq", "length", t, [ callee ], ?loc = r) |> Some - | "GetEnumerator", Some callee -> getEnumerator com r t i callee |> Some + | "get_Count", Some c -> libCall com r t "Seq" "length" [ c ] |> Some + | "GetEnumerator", Some c -> getEnumerator com r t i c |> Some | _ -> None let events (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3258,54 +2999,30 @@ let events (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E else "default" - Helper.LibCall(com, "Event", constructor, t, args, i.SignatureArgTypes, isConstructor = true, ?loc = r) - |> Some - | "get_Publish", Some callee -> getFieldWith r t callee "Publish" |> Some - | meth, Some callee -> makeInstanceCall r t i callee meth args |> Some - | meth, None -> - Helper.LibCall(com, "Event", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + libCallCons com r t i "Event" constructor args |> Some + | "get_Publish", Some c -> getFieldWith r t c "Publish" |> Some + | meth, Some c -> makeInstanceCall r t i c meth args |> Some + | meth, None -> makeLibCall com r t i "Event" (Naming.lowerFirst meth) args |> Some let observable (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = - Helper.LibCall(com, "Observable", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + makeLibCall com r t i "Observable" (Naming.lowerFirst i.CompiledName) args |> Some let mailbox (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg with | None -> match i.CompiledName with - | ".ctor" -> - Helper.LibCall( - com, - "MailboxProcessor", - "default", - t, - args, - i.SignatureArgTypes, - isConstructor = true, - ?loc = r - ) - |> Some - | "Start" -> - Helper.LibCall(com, "MailboxProcessor", "start", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ".ctor" -> libCallCons com r t i "MailboxProcessor" "default" args |> Some + | "Start" -> makeLibCall com r t i "MailboxProcessor" "start" args |> Some | _ -> None - | Some callee -> + | Some c -> match i.CompiledName with // `reply` belongs to AsyncReplyChannel - | "Start" - | "Receive" - | "PostAndAsyncReply" - | "Post" -> - let memb = - if i.CompiledName = "Start" then - "startInstance" - else - Naming.lowerFirst i.CompiledName - - Helper.LibCall(com, "MailboxProcessor", memb, t, args, i.SignatureArgTypes, thisArg = callee, ?loc = r) - |> Some - | "Reply" -> makeInstanceCall r t i callee "reply" args |> Some + | "Start" -> libCallThis com r t i "MailboxProcessor" "startInstance" thisArg args |> Some + | ("Receive" | "PostAndAsyncReply" | "Post") as meth -> + let meth = Naming.lowerFirst i.CompiledName + libCallThis com r t i "MailboxProcessor" meth thisArg args |> Some + | "Reply" -> makeInstanceCall r t i c "reply" args |> Some | _ -> None let tryGetAsyncDelayBinder = @@ -3319,49 +3036,25 @@ let tryGetAsyncDelayBinder = let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | "Singleton", _, _ -> Value(UnitConstant, r) |> Some - | "Bind", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Combine", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "combine", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Delay", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "delay", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "For", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "for_loop", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Return", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "r_return", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ReturnFrom", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "return_from", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TryFinally", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "try_finally", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "TryWith", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "try_with", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "Using", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "using", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Bind", _, _ -> makeLibCall com r t i "AsyncBuilder" "bind" args |> Some + | "Combine", _, _ -> makeLibCall com r t i "AsyncBuilder" "combine" args |> Some + | "Delay", _, _ -> makeLibCall com r t i "AsyncBuilder" "delay" args |> Some + | "For", _, _ -> makeLibCall com r t i "AsyncBuilder" "for_loop" args |> Some + | "Return", _, _ -> makeLibCall com r t i "AsyncBuilder" "r_return" args |> Some + | "ReturnFrom", _, _ -> makeLibCall com r t i "AsyncBuilder" "return_from" args |> Some + | "TryFinally", _, _ -> makeLibCall com r t i "AsyncBuilder" "try_finally" args |> Some + | "TryWith", _, _ -> makeLibCall com r t i "AsyncBuilder" "try_with" args |> Some + | "Using", _, _ -> makeLibCall com r t i "AsyncBuilder" "using" args |> Some | "While", _, [ guard; body ] -> match tryGetAsyncDelayBinder body with - | Some body -> - Helper.LibCall(com, "AsyncBuilder", "while_loop", t, [ guard; body ], i.SignatureArgTypes, ?loc = r) - |> Some + | Some body -> makeLibCall com r t i "AsyncBuilder" "while_loop" [ guard; body ] |> Some | None -> "Async builder While expects a delayed body." |> addErrorAndReturnNull com ctx.InlinePath r |> Some - | "Zero", _, _ -> - Helper.LibCall(com, "AsyncBuilder", "zero", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "AsyncBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Zero", _, _ -> makeLibCall com r t i "AsyncBuilder" "zero" args |> Some + | meth, Some c, _ -> makeInstanceCall r t i c meth args |> Some + | meth, None, _ -> makeLibCall com r t i "AsyncBuilder" (Naming.lowerFirst meth) args |> Some let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with @@ -3369,18 +3062,13 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis | "Start" -> "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r - Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "Async" "start" args |> Some // Make sure cancellationToken is called as a function and not a getter - | "get_CancellationToken" -> Helper.LibCall(com, "Async", "cancellationToken", t, [], ?loc = r) |> Some + | "get_CancellationToken" -> libCall com r t "Async" "cancellationToken" [] |> Some // `catch` cannot be used as a function name in JS - | "Catch" -> - Helper.LibCall(com, "Async", "catchAsync", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "Catch" -> makeLibCall com r t i "Async" "catchAsync" args |> Some // Fable.Core extensions - | meth -> - Helper.LibCall(com, "Async", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | meth -> makeLibCall com r t i "Async" (Naming.lowerFirst meth) args |> Some let tryGetTaskDelayBinder = function @@ -3393,51 +3081,29 @@ let tryGetTaskDelayBinder = let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", None, _ -> makeImportLib com t "new" "TaskBuilder" |> Some - | "task", _, _ -> Helper.LibCall(com, "TaskBuilder", "new", t, [], ?loc = r) |> Some - | "Run", Some callee, _ -> makeInstanceCall r t i callee "run" args |> Some - | ("Bind" | "TaskBuilderBase.Bind"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "bind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Combine" | "TaskBuilderBase.Combine"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "combine", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Delay" | "TaskBuilderBase.Delay"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "delay", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("For" | "TaskBuilderBase.For"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "for_loop", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Return" | "TaskBuilderBase.Return"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "r_return", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "task", _, _ -> libCall com r t "TaskBuilder" "new" [] |> Some + | "Run", Some c, _ -> makeInstanceCall r t i c "run" args |> Some + | ("Bind" | "TaskBuilderBase.Bind"), _, _ -> makeLibCall com r t i "TaskBuilder" "bind" args |> Some + | ("Combine" | "TaskBuilderBase.Combine"), _, _ -> makeLibCall com r t i "TaskBuilder" "combine" args |> Some + | ("Delay" | "TaskBuilderBase.Delay"), _, _ -> makeLibCall com r t i "TaskBuilder" "delay" args |> Some + | ("For" | "TaskBuilderBase.For"), _, _ -> makeLibCall com r t i "TaskBuilder" "for_loop" args |> Some + | ("Return" | "TaskBuilderBase.Return"), _, _ -> makeLibCall com r t i "TaskBuilder" "r_return" args |> Some | ("ReturnFrom" | "TaskBuilderBase.ReturnFrom"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "return_from", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "TaskBuilder" "return_from" args |> Some | ("TryFinally" | "TaskBuilderBase.TryFinally"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "try_finally", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("TryWith" | "TaskBuilderBase.TryWith"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "try_with", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | ("Using" | "TaskBuilderBase.Using"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "using", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + makeLibCall com r t i "TaskBuilder" "try_finally" args |> Some + | ("TryWith" | "TaskBuilderBase.TryWith"), _, _ -> makeLibCall com r t i "TaskBuilder" "try_with" args |> Some + | ("Using" | "TaskBuilderBase.Using"), _, _ -> makeLibCall com r t i "TaskBuilder" "using" args |> Some | ("While" | "TaskBuilderBase.While"), _, [ guard; body ] -> match tryGetTaskDelayBinder body with - | Some body -> - Helper.LibCall(com, "TaskBuilder", "while_loop", t, [ guard; body ], i.SignatureArgTypes, ?loc = r) - |> Some + | Some body -> makeLibCall com r t i "TaskBuilder" "while_loop" [ guard; body ] |> Some | None -> "Task builder While expects a delayed body." |> addErrorAndReturnNull com ctx.InlinePath r |> Some - | ("Zero" | "TaskBuilderBase.Zero"), _, _ -> - Helper.LibCall(com, "TaskBuilder", "zero", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | meth, Some callee, _ -> makeInstanceCall r t i callee meth args |> Some - | meth, None, _ -> - Helper.LibCall(com, "TaskBuilder", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | ("Zero" | "TaskBuilderBase.Zero"), _, _ -> makeLibCall com r t i "TaskBuilder" "zero" args |> Some + | meth, Some c, _ -> makeInstanceCall r t i c meth args |> Some + | meth, None, _ -> makeLibCall com r t i "TaskBuilder" (Naming.lowerFirst meth) args |> Some let guids (com: ICompiler) @@ -3451,7 +3117,7 @@ let guids match i.CompiledName, thisArg, args with | ".ctor", None, _ -> match args with - | [] -> Helper.LibCall(com, "Guid", "empty", t, [], ?loc = r) |> Some + | [] -> libCall com r t "Guid" "empty" [] |> Some | [ ExprType String ] -> makeMemberCall com ctx r t i "Guid" "parse" None args |> Some | [ ExprType(Array(Number(UInt8, _), _)) ] -> makeMemberCall com ctx r t i "Guid" "new_from_array" thisArg args |> Some @@ -3478,54 +3144,25 @@ let uris (thisArg: Expr option) (args: Expr list) = - match i.CompiledName with - | ".ctor" -> + match i.CompiledName, thisArg with + | ".ctor", _ -> match args with - | [ ExprType String ] -> - Helper.LibCall(com, "Uri", "create", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ ExprType String; _ ] -> - Helper.LibCall(com, "Uri", "createWithKind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ _; ExprType String ] -> - Helper.LibCall(com, "Uri", "createFromString", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | [ _; _ ] -> - Helper.LibCall(com, "Uri", "createFromUri", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | [ ExprType String ] -> makeLibCall com r t i "Uri" "create" args |> Some + | [ ExprType String; _ ] -> makeLibCall com r t i "Uri" "createWithKind" args |> Some + | [ _; ExprType String ] -> makeLibCall com r t i "Uri" "createFromString" args |> Some + | [ _; _ ] -> makeLibCall com r t i "Uri" "createFromUri" args |> Some | _ -> None - | "TryCreate" -> + | "TryCreate", _ -> match args with - | (ExprType String) :: _ -> - Helper.LibCall(com, "Uri", "tryCreateWithKind", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | _ :: (ExprType String) :: _ -> - Helper.LibCall(com, "Uri", "tryCreateFromString", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | _ -> - Helper.LibCall(com, "Uri", "tryCreateFromUri", t, args, i.SignatureArgTypes, ?loc = r) - |> Some - | "ToString" -> toString com ctx r [ thisArg.Value ] |> Some - | "UnescapeDataString" -> - Helper.LibCall(com, "Uri", "unescapeDataString", t, args, i.SignatureArgTypes) - |> Some - | "EscapeDataString" -> - Helper.LibCall(com, "Uri", "escapeDataString", t, args, i.SignatureArgTypes) - |> Some - | "EscapeUriString" -> - Helper.LibCall(com, "Uri", "escapeUriString", t, args, i.SignatureArgTypes) - |> Some - | "get_IsAbsoluteUri" - | "get_Scheme" - | "get_Host" - | "get_Port" - | "get_IsDefaultPort" - | "get_AbsolutePath" - | "get_AbsoluteUri" - | "get_PathAndQuery" - | "get_Query" - | "get_Fragment" - | "get_OriginalString" -> makeMemberCall com ctx r t i "Uri" i.CompiledName thisArg args |> Some + | (ExprType String) :: _ -> makeLibCall com r t i "Uri" "tryCreateWithKind" args |> Some + | _ :: (ExprType String) :: _ -> makeLibCall com r t i "Uri" "tryCreateFromString" args |> Some + | _ -> makeLibCall com r t i "Uri" "tryCreateFromUri" args |> Some + | "ToString", Some c -> toString com ctx r [ c ] |> Some + | "UnescapeDataString", _ -> makeLibCall com r t i "Uri" "unescapeDataString" args |> Some + | "EscapeDataString", _ -> makeLibCall com r t i "Uri" "escapeDataString" args |> Some + | "EscapeUriString", _ -> makeLibCall com r t i "Uri" "escapeUriString" args |> Some + | ("get_IsAbsoluteUri" | "get_Scheme" | "get_Host" | "get_Port" | "get_IsDefaultPort" | "get_AbsolutePath" | "get_AbsoluteUri" | "get_PathAndQuery" | "get_Query" | "get_Fragment" | "get_OriginalString"), + thisArg -> makeMemberCall com ctx r t i "Uri" i.CompiledName thisArg args |> Some | _ -> None let laziness (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3537,9 +3174,9 @@ let laziness (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op // // | "CreateFromValue", _, _ -> // // Helper.LibCall(com, "Native", "lazyFromValue", t, args, i.SignatureArgTypes, ?loc = r) // // |> Some -// | ("Force" | "get_Value"), Some callee, _ -> makeInstanceCall r t i callee "force" [] |> Some -// // | "get_IsValueCreated", Some callee, _ -> -// // Naming.removeGetSetPrefix i.CompiledName |> getFieldWith r t callee |> Some +// | ("Force" | "get_Value"), Some c, _ -> makeInstanceCall r t i c "force" [] |> Some +// // | "get_IsValueCreated", Some c, _ -> +// // Naming.removeGetSetPrefix i.CompiledName |> getFieldWith r t c |> Some // | _ -> None let controlExtensions @@ -3562,7 +3199,7 @@ let controlExtensions |> Option.defaultValue (args, i.SignatureArgTypes) |> fun (args, argTypes) -> List.rev args, List.rev argTypes - Helper.LibCall(com, "Observable", meth, t, args, argTypes) + libCallTyped com None t "Observable" meth args argTypes ) let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3667,16 +3304,12 @@ let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio match resolved, thisArg with | Some _, _ -> resolved - | None, Some thisArg -> + | None, Some c -> match i.CompiledName with - | "GetTypeInfo" -> Some thisArg + | "GetTypeInfo" -> Some c | "get_GenericTypeArguments" - | "GetGenericArguments" -> - Helper.LibCall(com, "Reflection", "getGenerics", t, [ thisArg ], ?loc = r) - |> Some - | "MakeGenericType" -> - Helper.LibCall(com, "Reflection", "makeGenericType", t, thisArg :: args, ?loc = r) - |> Some + | "GetGenericArguments" -> libCall com r t "Reflection" "getGenerics" [ c ] |> Some + | "MakeGenericType" -> libCall com r t "Reflection" "makeGenericType" (c :: args) |> Some | "get_FullName" | "get_Namespace" | "get_IsArray" @@ -3691,7 +3324,7 @@ let types (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | "IsInstanceOfType" -> let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst - Helper.LibCall(com, "Reflection", meth, t, thisArg :: args, ?loc = r) |> Some + libCall com r t "Reflection" meth (c :: args) |> Some | _ -> None | None, None -> None @@ -3701,18 +3334,14 @@ let fsharpType com methName (r: SourceLocation option) t (i: CallInfo) (args: Ex Helper.LibCall(com, "Reflection", "tuple_type", t, args, i.SignatureArgTypes, hasSpread = true, ?loc = r) |> Some // Prevent name clash with FSharpValue.GetRecordFields - | "GetRecordFields" -> - Helper.LibCall(com, "Reflection", "getRecordElements", t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "GetRecordFields" -> makeLibCall com r t i "Reflection" "getRecordElements" args |> Some | "GetUnionCases" | "GetTupleElements" | "GetFunctionElements" | "IsUnion" | "IsRecord" | "IsTuple" - | "IsFunction" -> - Helper.LibCall(com, "Reflection", Naming.lowerFirst methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "IsFunction" -> makeLibCall com r t i "Reflection" (Naming.lowerFirst methName) args |> Some | "IsExceptionRepresentation" | "GetExceptionFields" -> None // TODO!!! | _ -> None @@ -3738,31 +3367,28 @@ let fsharpValue com methName (r: SourceLocation option) t (i: CallInfo) (args: E record :: typeInfo :: rest, argTypes | _ -> args, i.SignatureArgTypes - Helper.LibCall(com, "Reflection", "getRecordFields", t, args, argTypes, ?loc = r) - |> Some + libCallTyped com r t "Reflection" "getRecordFields" args argTypes |> Some | "GetUnionFields" | "GetRecordField" | "GetTupleFields" | "GetTupleField" | "MakeUnion" | "MakeRecord" - | "MakeTuple" -> - Helper.LibCall(com, "Reflection", Naming.lowerFirst methName, t, args, i.SignatureArgTypes, ?loc = r) - |> Some + | "MakeTuple" -> makeLibCall com r t i "Reflection" (Naming.lowerFirst methName) args |> Some | "GetExceptionFields" -> None // TODO!!! | _ -> None let tryField com t ownerTyp fieldName = match ownerTyp, fieldName with - | Number(Decimal, _), _ -> Helper.LibValue(com, "Decimal", fieldName, t) |> Some + | Number(Decimal, _), _ -> libValue com None t "Decimal" fieldName [] |> Some | String, "Empty" -> makeStrConst "" |> Some - | Builtin BclGuid, "Empty" -> Helper.LibValue(com, "Guid", "empty", t) |> Some + | Builtin BclGuid, "Empty" -> libValue com None t "Guid" "empty" [] |> Some | Builtin BclTimeSpan, _ -> let meth = fieldName |> Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase - Helper.LibValue(com, "TimeSpan", meth, t) |> Some + libValue com None t "TimeSpan" meth [] |> Some | Builtin BclRune, "ReplacementChar" - | Builtin BclRune, "get_ReplacementChar" -> Helper.LibValue(com, "Rune", "replacementChar", t) |> Some + | Builtin BclRune, "get_ReplacementChar" -> libValue com None t "Rune" "replacementChar" [] |> Some | Builtin BclDateTime, _ -> let meth = fieldName |> Naming.lowerFirst makeStaticFieldCall com None t "DateTime" "DateTime" meth |> Some @@ -3774,7 +3400,7 @@ let tryField com t ownerTyp fieldName = let meth = fieldName |> Naming.lowerFirst match ent.FullName with - | "System.BitConverter" -> Helper.LibCall(com, "BitConverter", meth, t, []) |> Some + | "System.BitConverter" -> libCall com None t "BitConverter" meth [] |> Some | _ -> None | _ -> None @@ -3877,13 +3503,13 @@ let private replacedModules = Types.datetimeOffset, dateTimeOffsets Types.dateOnly, dateOnly Types.timeOnly, timeOnly - Types.rune, rune + Types.rune, runes Types.timespan, timeSpans "System.Timers.Timer", timers "System.Environment", systemEnv "System.IO.File", files "System.IO.Path", paths - "System.Globalization.CultureInfo", globalization + Types.cultureInfo, globalization "System.Random", random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels @@ -3966,8 +3592,8 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr // UnionCaseInfo from a NewUnionCase quotation deconstruction is a Rust-native // FSharpUnionCaseInfo carrier; route member access to the quotation runtime. match thisArg, info.CompiledName with - | Some c, "get_Name" -> Helper.LibCall(com, "quotation", "unionCaseName", t, [ c ], ?loc = r) |> Some - | Some c, "get_Tag" -> Helper.LibCall(com, "quotation", "unionCaseTag", t, [ c ], ?loc = r) |> Some + | Some c, "get_Name" -> libCall com r t "quotation" "unionCaseName" [ c ] |> Some + | Some c, "get_Tag" -> libCall com r t "quotation" "unionCaseTag" [ c ] |> Some | _ -> None | "System.Reflection.PropertyInfo" | "System.Reflection.ParameterInfo" @@ -3985,26 +3611,22 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr | _ -> false match thisArg, info.CompiledName with - | Some c, "get_Name" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodName", t, [ c ], ?loc = r) |> Some + | Some c, "get_Name" when isMethodInfoCarrier c -> libCall com r t "quotation" "methodName" [ c ] |> Some | Some c, "get_DeclaringType" when isMethodInfoCarrier c -> - Helper.LibCall(com, "quotation", "methodDeclaringType", t, [ c ], ?loc = r) - |> Some + libCall com r t "quotation" "methodDeclaringType" [ c ] |> Some | Some c, "get_Tag" -> makeStrConst "tag" |> getExpr r t c |> Some | Some c, "get_ReturnType" -> makeStrConst "returnType" |> getExpr r t c |> Some | Some c, "GetParameters" -> makeStrConst "parameters" |> getExpr r t c |> Some | Some c, ("get_PropertyType" | "get_ParameterType") -> makeIntConst 1 |> getExpr r t c |> Some - | Some c, "GetFields" -> - Helper.LibCall(com, "Reflection", "getUnionCaseFields", t, [ c ], ?loc = r) - |> Some - | Some c, "GetValue" -> Helper.LibCall(com, "Reflection", "getValue", t, c :: args, ?loc = r) |> Some + | Some c, "GetFields" -> libCall com r t "Reflection" "getUnionCaseFields" [ c ] |> Some + | Some c, "GetValue" -> libCall com r t "Reflection" "getValue" (c :: args) |> Some | Some c, "get_Name" -> match c with | Value(TypeInfo(exprType, _), loc) -> getTypeName com ctx loc exprType |> StringConstant |> makeValue r |> Some // Runtime PropertyInfo (e.g. a record field info): read its carried name. // Distinct from the generic type-name helper `name()` to avoid a clash. - | c -> Helper.LibCall(com, "Reflection", "propertyName", t, [ c ], ?loc = r) |> Some + | c -> libCall com r t "Reflection" "propertyName" [ c ] |> Some | _ -> None // F# Quotations | typeName -> Quotations.tryQuotationCall "quotation" com ctx r t info thisArg args typeName @@ -4047,7 +3669,7 @@ let tryType typ = | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffsets, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) - | BclRune -> Some(Types.rune, rune, []) + | BclRune -> Some(Types.rune, runes, []) | BclTimer -> Some("System.Timers.Timer", timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 6621fc3ef5..5f0d7ae2b4 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -474,6 +474,9 @@ module Types = [] let iformatProvider = "System.IFormatProvider" + [] + let cultureInfo = "System.Globalization.CultureInfo" + [] let iobserverGeneric = "System.IObserver`1" diff --git a/src/fable-library-rust/src/Decimal.rs b/src/fable-library-rust/src/Decimal.rs index f1d6bc2226..60f521b6a4 100644 --- a/src/fable-library-rust/src/Decimal.rs +++ b/src/fable-library-rust/src/Decimal.rs @@ -36,6 +36,10 @@ pub mod Decimal_ { pub fn abs(x: decimal) -> decimal { x.abs() } pub fn sign(x: decimal) -> i32 { x.signum().to_i32().unwrap() } + pub fn copySign(x: decimal, sign: decimal) -> decimal { + if x.is_sign_negative() == sign.is_sign_negative() { x } else { -x } + } + pub fn max(x: decimal, y: decimal) -> decimal { x.max(y) } pub fn min(x: decimal, y: decimal) -> decimal { x.min(y) } @@ -83,6 +87,14 @@ pub mod Decimal_ { pub fn toDecimal(x: decimal) -> decimal { x } + pub fn toOACurrency(x: decimal) -> i64 { + let scaled = x * Decimal::from_i32(10000).unwrap(); + scaled + .round_dp_with_strategy(0, RoundingStrategy::MidpointNearestEven) + .to_i64() + .unwrap() + } + pub fn toBoolean(x: decimal) -> bool { !x.is_zero() } pub fn toChar(x: decimal) -> char { @@ -93,6 +105,8 @@ pub mod Decimal_ { toString_1(&x) } + pub fn getTypeCode() -> i32 { 15 } + pub fn tryParse(s: string, res: &MutCell) -> bool { match Decimal::from_str(s.trim()) { Ok(d) => { res.set(d); true }, @@ -123,6 +137,10 @@ pub mod Decimal_ { pub fn fromDecimal(d: decimal) -> decimal { d } + pub fn fromOACurrency(value: i64) -> decimal { + Decimal::from_i64(value).unwrap() / Decimal::from_i32(10000).unwrap() + } + pub fn fromBoolean(b: bool) -> decimal { Decimal::from_u32(b as u32).unwrap() } @@ -190,13 +208,9 @@ pub mod Decimal_ { roundToMode(x, 0, mode) } - // pub fn copySign // pub fn createChecked // pub fn createSaturating // pub fn createTruncating - // pub fn fromOACurrency - // pub fn getTypeCode - // pub fn toOACurrency // pub fn tryFormat // pub fn tryGetBits } diff --git a/tests/Rust/tests/src/ArithmeticTests.fs b/tests/Rust/tests/src/ArithmeticTests.fs index 3acf821fe8..0debfebb99 100644 --- a/tests/Rust/tests/src/ArithmeticTests.fs +++ b/tests/Rust/tests/src/ArithmeticTests.fs @@ -173,17 +173,6 @@ let ``Decimal literals can be generated`` () = 79228162514264337593543950335M |> equal Decimal.MaxValue -79228162514264337593543950335M |> equal Decimal.MinValue -[] -let ``Decimal integer predicates work`` () = - System.Decimal.IsInteger(3M) |> equal true - System.Decimal.IsInteger(3.5M) |> equal false - System.Decimal.IsEvenInteger(4M) |> equal true - System.Decimal.IsEvenInteger(3M) |> equal false - System.Decimal.IsOddInteger(3M) |> equal true - System.Decimal.IsOddInteger(4M) |> equal false - System.Decimal.IsOddInteger(-3M) |> equal true - System.Decimal.IsOddInteger(3.5M) |> equal false - [] let ``Decimal.IsCanonical works`` () = // Canonical means the value's stored scale is already minimal (no representable @@ -197,6 +186,102 @@ let ``Decimal.IsCanonical works`` () = System.Decimal.IsCanonical(1.20M) |> equal false System.Decimal.IsCanonical(0.0M) |> equal false +[] +let ``Decimal.IsInteger works`` () = + System.Decimal.IsInteger(3M) |> equal true + System.Decimal.IsInteger(3.5M) |> equal false + System.Decimal.IsInteger(-3M) |> equal true + System.Decimal.IsInteger(-3.5M) |> equal false + +[] +let ``Decimal.IsEvenInteger works`` () = + System.Decimal.IsEvenInteger(3M) |> equal false + System.Decimal.IsEvenInteger(4M) |> equal true + System.Decimal.IsEvenInteger(-3M) |> equal false + System.Decimal.IsEvenInteger(-4M) |> equal true + System.Decimal.IsEvenInteger(3.5M) |> equal false + System.Decimal.IsEvenInteger(-3.5M) |> equal false + +[] +let ``Decimal.IsOddInteger works`` () = + System.Decimal.IsOddInteger(3M) |> equal true + System.Decimal.IsOddInteger(4M) |> equal false + System.Decimal.IsOddInteger(-3M) |> equal true + System.Decimal.IsOddInteger(-4M) |> equal false + System.Decimal.IsOddInteger(3.5M) |> equal false + System.Decimal.IsOddInteger(-3.5M) |> equal false + +[] +let ``Decimal.IsNegative works`` () = + System.Decimal.IsNegative(-1M) |> equal true + System.Decimal.IsNegative(1M) |> equal false + +[] +let ``Decimal.IsPositive works`` () = + System.Decimal.IsPositive(1M) |> equal true + System.Decimal.IsPositive(-1M) |> equal false + +[] +let ``Decimal.Scale works`` () = + 1M.Scale |> equal 0uy + 1.20M.Scale |> equal 2uy + +[] +let ``Decimal.CopySign works`` () = + Decimal.CopySign(1M, 2M) |> equal 1M + Decimal.CopySign(-1M, 2M) |> equal 1M + Decimal.CopySign(1M, -2M) |> equal -1M + Decimal.CopySign(-1M, -2M) |> equal -1M + +[] +let ``Decimal.FromOACurrency works`` () = + Decimal.FromOACurrency(0L) |> equal 0M + Decimal.FromOACurrency(1L) |> equal 0.0001M + Decimal.FromOACurrency(100000L) |> equal 10M + Decimal.FromOACurrency(100000000000L) |> equal 10000000M + Decimal.FromOACurrency(1000000000000000000L) |> equal 100000000000000M + Decimal.FromOACurrency(1000000000000000001L) |> equal 100000000000000.0001M + Decimal.FromOACurrency(Int64.MaxValue) |> equal 922337203685477.5807M + Decimal.FromOACurrency(Int64.MinValue) |> equal -922337203685477.5808M + Decimal.FromOACurrency(123456L) |> equal 12.3456M + Decimal.FromOACurrency(123456789L) |> equal 12345.6789M + Decimal.FromOACurrency(1234567890000L) |> equal 123456789M + Decimal.FromOACurrency(1234567890987654321L) |> equal 123456789098765.4321M + Decimal.FromOACurrency(4294967295L) |> equal 429496.7295M + +[] +let ``Decimal.ToOACurrency works`` () = + Decimal.ToOACurrency(0M) |> equal 0L + Decimal.ToOACurrency(1M) |> equal 10000L + Decimal.ToOACurrency(1.0000000000000000000000000000M) |> equal 10000L + Decimal.ToOACurrency(100000000000000M) |> equal 1000000000000000000L + Decimal.ToOACurrency(100000000000000.00000000000000M) |> equal 1000000000000000000L + throwsAnyError (fun () -> Decimal.ToOACurrency(10000000000000000000000000000M) |> ignore) + Decimal.ToOACurrency(0.000000000123456789M) |> equal 0L + Decimal.ToOACurrency(0.123456789M) |> equal 1235L + Decimal.ToOACurrency(12.3456M) |> equal 123456L + Decimal.ToOACurrency(123456789M) |> equal 1234567890000L + throwsAnyError (fun () -> Decimal.ToOACurrency(123456789000000000M) |> ignore) + Decimal.ToOACurrency(4294967295M) |> equal 42949672950000L + throwsAnyError (fun () -> Decimal.ToOACurrency(18446744073709551615M) |> ignore) + Decimal.ToOACurrency(-79.228162514264337593543950335M) |> equal -792282L + Decimal.ToOACurrency(-79228162514264.337593543950335M) |> equal -792281625142643376L + +[] +let ``Decimal.GetTypeCode works`` () = + 1M.GetTypeCode() |> equal TypeCode.Decimal + +[] +let ``Decimal.Abs works`` () = + Decimal.Abs(4M) |> equal 4M + Decimal.Abs(-4M) |> equal 4M + +[] +let ``Decimal.Sign works`` () = + Decimal.Sign(-1M) |> equal -1 + Decimal.Sign(0M) |> equal 0 + Decimal.Sign(1M) |> equal 1 + [] let ``Decimal.ToString works`` () = string 001.23456M |> equal "1.23456" From 58b79e0a7c03c17aa4aa4981e59621d0559a9a0b Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 27 Sep 2026 17:41:59 +0200 Subject: [PATCH 098/322] fix(beam): honor custom ToString in %O (#4994) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 117 +++++++++++++++++++--- src/Fable.Transforms/Beam/Fable2Beam.fs | 16 +-- src/Fable.Transforms/Beam/Prelude.fs | 95 +++++++++++++++--- src/Fable.Transforms/Beam/Replacements.fs | 63 ++++++------ src/fable-library-beam/fable_string.erl | 50 ++++++--- tests/Beam/RecordTests.fs | 24 +++++ tests/Beam/UnionTypeTests.fs | 23 +++++ 7 files changed, 310 insertions(+), 78 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 11d8cb296d..2da0eb65b5 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -385,15 +385,18 @@ is reached, and these degrade to the generic case below: ```fsharp box c |> string // "50" — the pipe binds it as obj first let b: obj = box c in string b // "50" -sprintf "%O" c // "50" — printf applies its arguments through a curried runtime +sprintf "%O" c // "50" — no char converter is currently threaded to printf sprintf "%A" c // "50" — .NET gives "'2'" [ box '2' ] |> List.map string // ["50"] — element type is obj ``` The `%O`/`%A` cases are not a boxing problem but a printf one: `printf` parses its format string and -applies its arguments at runtime, so no argument's static type reaches the formatter at all. Fixing -those means threading per-argument type info from the call site — see "Structured formatting (`%A`)" -below. +applies its arguments through a curried runtime function. The compiler now preserves one narrow +piece of static information across that boundary: when a concrete argument type overrides +`System.Object.ToString()`, it supplies a converter for that argument and `%O` invokes it. This fixes +custom record and union formatting without changing their runtime representation. It does not yet +supply converters for primitive representations such as `char`, or the richer type information +needed by `%A`; see "Object overrides and printf" and "Structured formatting (`%A`)" below. But when `string x` is applied where `x`'s type is a *generic parameter*, the backend can only emit `fable_convert:to_string/1`, which sees an integer and prints the number: @@ -433,6 +436,81 @@ breaking change to a core type, costs arithmetic and comparison performance, and convention that strings are lists of integer codepoints. Not worth it for a case with a one-keyword workaround. +### Object overrides and printf + +Records and unions are still bare maps, tuples, or atoms, so a runtime value cannot dynamically +look up an attached method. Custom `System.Object.ToString()` overrides are instead resolved while +the concrete F# type is available: + +- `ObjectOverrides` in `Prelude.fs` owns override detection, generated function naming, and + zero-argument call construction. +- Declaration emission keeps `System.Object` overrides as module-level functions instead of putting + them in an instance dispatch map that a record or union value cannot carry. +- Direct `value.ToString()` and `string value` use `ToString.toStringByType`, which first tries the + statically resolved override and then applies the built-in type-specific conversions. +- `PrintfFormat<'Printer, _, _, _>` exposes its argument types as a nested lambda chain. The + `PrintfFormat` replacement uses those types to build a parallel list containing a custom + `ToString` converter or `undefined` for each argument. `fable_string:printf/2` carries the list + through the curried printer and applies a converter only for `%O`; `printf/1` remains the + backward-compatible path when no converters are needed. All consumers of that format object, + including `sprintf`, `printfn`, `eprintfn`, and `failwithf`, share the behavior. + +For example, both expressions below now produce `Untrusted` on Beam, matching .NET: + +```fsharp +type IntegrityLevel = + | Untrusted + | Trusted + + override this.ToString() = + match this with + | Untrusted -> "Untrusted" + | Trusted -> "Trusted" + +IntegrityLevel.Untrusted.ToString() +sprintf "%O" IntegrityLevel.Untrusted +``` + +The Beam record and union tests cover direct calls, the `string` operator, `%O`, mixed format +arguments, partial application, and a cross-file call. This keeps static override lookup, generated +imports, and the printf transport from regressing independently across the atom/tuple and map +representations. + +This is intentionally static dispatch. If the value has already been erased to `obj`, or its type +is an uninlined generic parameter, the call site no longer identifies the declaring type and the +runtime fallback remains responsible for formatting it. The optional formatter list also currently +tracks the `PrintfFormat` argument sequence directly. Any future generalization must account for +format specifiers such as `%a`, `%t`, and `%*d`, whose consumed arguments do not have a simple +one-specifier/one-value correspondence. + +#### Proposed follow-up: an argument-slot formatting plan + +The optional converter list is a deliberately narrow bridge for custom `%O`; it should not grow +into a collection of unrelated formatter flags. Its entries follow the `PrintfFormat` argument type +sequence, while the runtime currently consumes one entry per parsed format specifier. That happens +to align for ordinary `%O`, but it is not the general printf model: + +- `%O`, `%A`, and `%d` consume one value argument. +- `%*d` consumes a width and a value. +- `%a` consumes a printer and a value. +- `%t` consumes a thunk. + +Before adding type-directed formatting for more cases, replace the simple correspondence with an +explicit argument-slot plan. The compiler should build the plan centrally from the format and its +`PrintfFormat` lambda chain. Each slot describes its role (value, width, printer, or thunk) and may +carry a statically selected value formatter. The runtime then consumes slots according to the +specifier's actual arity instead of assuming one specifier equals one argument. + +The current custom `ToString` closure becomes the first value-formatter hint in that plan. Later +hints can cover representation-ambiguous primitives such as `char`, `decimal`, `DateTime`, and +`TimeSpan`, and richer descriptors or generated recursive formatters can support `%A`. String +interpolation, `System.String.Format`, and `Console` overloads should reuse the same centralized +static formatting selection rather than acquire independent type-dispatch tables. + +Dynamic formats, values already erased to `obj`, and uninlined generic parameters still need the +runtime shape-based fallback. The plan transports information available at a typed call site; it +does not make values self-describing or change their Erlang representation. + ### Structured formatting (`%A`) reads shapes, not types `%A` renders a value in F# syntax. The other targets get this for free because their generated types @@ -466,13 +544,24 @@ the wrong style but is unreadable. They are still shape collisions rather than b the runtime term says "this reference is a ref cell, not an array" or "this integer is scaled" — and all four print at least as well as the `~p` dump they replaced. -Recovering the first three needs the argument's static type threaded from the `%A` call site, where -it does still exist: `printfn`'s `PrintfFormat<'Printer, _, _, _>` carries the per-argument types as -a `LambdaType` chain in `CallInfo.GenericArgs`, which `NestedLambdaType` decomposes. That would mean -emitting a `makeTypeInfo` per argument in `fsFormat` and pairing each format specifier with it in -`create_printer` — machinery no Fable target has today, and it would still need a fallback for -generic parameters (which erase to a placeholder) and for `%a`/`%t`/`%*d` specifiers whose arity does -not line up with the value list. +Recovering these cases needs more than the optional `ToString` converters now threaded for `%O`. +The argument-slot plan described above should carry compact type descriptors or generated recursive +formatters for `%A`; full reflection maps are unnecessary. Generic parameters that erase to a +placeholder still use the shape-based fallback. + +Persisting types in every runtime object is the alternative. A stable type token on records, +unions, and class instances could support dynamic override dispatch, reflection, serializers, +type tests, and unambiguous recursive `%A` formatting even after values flow through `obj`. But this +is not a local formatting fix: nullary union cases could no longer remain bare atoms, records and +union values would need wrappers or reserved metadata fields, and construction, pattern matching, +equality, comparison, hashing, JSON/interop, and package compatibility would all be affected. A +full type-info value is also a poor payload because it may contain functions and other terms with +awkward equality semantics; a compact token plus a registry or generated reflection function would +be safer. + +For now, Beam keeps its compact idiomatic representations and threads only the static information a +specific call site needs. Self-describing values remain a possible future ABI revision rather than +an incremental extension of the current object schema. Anything the formatter does not recognise — pids, ports, a cyclic term past the depth cap — falls back to `~tp`, which is what `%A` did for *everything* before, so it can never be worse than it was. @@ -1315,12 +1404,14 @@ alone eliminates the single hardest piece of the Fable.Python runtime. pattern used for Call/Apply/BinOp arguments. - **sprintf / printfn / String.Format**: Full F# format string support via `fable_string.erl` runtime. `printf/1` parses format strings (`%d`, `%s`, `%.2f`, `%g`, `%x`, etc.) into a - continuation-based format object `#{input, cont}`. `to_text` (sprintf), `to_console` + continuation-based format object `#{input, cont}`. When a statically known argument type has a + custom `System.Object.ToString()` override, the compiler calls `printf/2` with a parallel list of + optional converters; the runtime applies them only to `%O`. `to_text` (sprintf), `to_console` (printfn), `to_console_error` (eprintfn), `to_fail` (failwithf) apply continuations with appropriate handlers. Multi-arity overloads (`to_text/1..5`) handle Fable's inlined arg passing where `CurriedApply` flattens curried args into a single call. `format/2` handles .NET `String.Format("{0} {1}", args)` with positional placeholders. Replacements routing: - `fsFormat` function handles `PrintfFormat.ctor` (→ `printf`), `PrintFormatToString` + `fsFormat` function handles `PrintfFormat.ctor` (→ `printf/1` or `printf/2`), `PrintFormatToString` (→ `to_text`), `PrintFormatLine`/`PrintFormat` (→ `to_console`), etc. Dispatched from both `operators` (for `ExtraTopLevelOperators.sprintf`) and `tryCall` (for `PrintfModule`/ `PrintfFormat` entities). The old `toConsole` → `io:format("~s~n")` hack in Fable2Beam.fs diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 3009193d5f..7d1166db91 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -4,6 +4,7 @@ open Fable open Fable.AST open Fable.AST.Fable open Fable.Transforms +open Fable.Beam open Fable.Beam.Naming open Fable.Transforms.Beam.Util @@ -2898,7 +2899,7 @@ and transformClassDeclaration decl.AttachedMembers |> List.choose (fun memb -> match memb.ImplementedSignatureRef with - | Some sigRef -> + | Some sigRef when not (ObjectOverrides.isSystemObjectOverride memb) -> let sigInfo = com.GetMember(sigRef) let memberName = sanitizeErlangName memb.Name @@ -2987,6 +2988,7 @@ and transformClassDeclaration memberName Some(atomLit keyName, closure) + | Some _ | None -> None ) @@ -3114,9 +3116,12 @@ and transformClassDeclaration let memberForms = decl.AttachedMembers |> List.collect (fun memb -> - // Skip interface implementations — they are inlined as closures - // in interfaceEntries and don't need separate module-level functions. - if memb.ImplementedSignatureRef.IsSome then + // Interface implementations are inlined as closures in interfaceEntries. System.Object + // overrides remain module-level functions because records and unions have no instance map. + if + memb.ImplementedSignatureRef.IsSome + && not (ObjectOverrides.isSystemObjectOverride memb) + then [] else @@ -3254,8 +3259,7 @@ and transformClassDeclaration [ Beam.ErlForm.Function funcDef ] else // Regular method: class_name_method(This, Args...) -> Body. - let methodName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_%s{methodName}" + let funcName = ObjectOverrides.functionName className memb.Name let _thisArg, nonThisArgs, memberCtx = getThisAndArgs () let argPatterns = diff --git a/src/Fable.Transforms/Beam/Prelude.fs b/src/Fable.Transforms/Beam/Prelude.fs index ddf4688427..4a17f395bb 100644 --- a/src/Fable.Transforms/Beam/Prelude.fs +++ b/src/Fable.Transforms/Beam/Prelude.fs @@ -548,6 +548,64 @@ module Chars = | Some _ -> Char | None -> e.Type +/// Statically resolved implementations of virtual System.Object members. +/// +/// Records and unions are bare maps/tuples/atoms on Beam, so they cannot carry an instance method +/// table. Keep the generated function naming and lookup contract in one place: declaration emission +/// uses the same name that call sites import. +module ObjectOverrides = + open Fable + open Fable.AST + open Fable.AST.Fable + open Fable.Transforms + + let isSystemObjectOverride (memb: MemberDecl) = + match memb.ImplementedSignatureRef with + | Some(MemberRef(entityRef, _)) -> entityRef.FullName = Types.object + | _ -> false + + let functionName entityName memberName = + $"%s{entityName}_%s{Naming.sanitizeErlangName memberName}" + + let tryCallZeroArg + (com: FSharp2Fable.IFableCompiler) + (r: SourceLocation option) + (returnType: Type) + memberName + (arg: Expr) + = + match arg.Type with + | DeclaredType(entRef, _) -> + let memberInfo = + { + IsInstance = true + CompiledName = memberName + NonCurriedArgTypes = Some [] + AttributeFullNames = [] + } + + com.TryGetEntity(entRef) + |> Option.bind (fun ent -> ent.TryFindMember(memberInfo)) + |> Option.filter (fun memb -> memb.IsOverrideOrExplicitInterfaceImplementation) + |> Option.bind (fun memb -> + entRef.SourcePath + |> Option.map (fun sourcePath -> + let entityName = FSharp2Fable.Helpers.getEntityDeclarationName com entRef + let calleeName = functionName entityName memb.CompiledName + let calleeType = LambdaType(arg.Type, returnType) + let memberRef = MemberRef(entRef, memberInfo) + + let callee = + if sourcePath = com.CurrentFile && not com.IsPrecompilingInlineFunction then + makeTypedIdentExpr calleeType calleeName + else + makeInternalMemberImport com calleeType memberRef calleeName sourcePath r + + makeCall r returnType (makeCallInfo None [ arg ] [ arg.Type ]) callee + ) + ) + | _ -> None + /// Conversion of a value to its `string` form, dispatched on the static type. module ToString = open Fable.AST @@ -568,18 +626,31 @@ module ToString = let toStringByType (com: ICompiler) (r: SourceLocation option) (t: Type) (arg: Expr) = let arg = Chars.tryAsChar arg |> Option.defaultValue arg - match arg.Type with - | Type.String -> Some arg - | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some - | Float16 - | Float32 - | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some - | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some - | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + match ObjectOverrides.tryCallZeroArg com r t "ToString" arg with + | Some call -> Some call + | None -> + match arg.Type with + | Type.String -> Some arg + | Type.Char -> emitExpr r t [ arg ] "<<($0)/utf8>>" |> Some + | Type.Number(kind, _) -> + match kind with + | Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some + | Float16 + | Float32 + | Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some + | _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some + | Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some + | DeclaredType(ent, _) when ent.FullName = Types.timespan -> + Helper.LibCall(com, "fable_timespan", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = Types.datetime -> + Helper.LibCall(com, "fable_date", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = "System.Uri" -> + Helper.LibCall(com, "fable_uri", "to_string", t, [ arg ], ?loc = r) |> Some + | DeclaredType(ent, _) when ent.FullName = "System.Text.StringBuilder" -> + // StringBuilder.ToString() → iolist_to_binary(get(maps:get(field_buf, get(Sb)))) + emitExpr r t [ arg ] "iolist_to_binary(get(maps:get(field_buf, get($0))))" + |> Some + | _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some /// Fixed-width integer semantics. Erlang integers are arbitrary precision and never /// overflow, so operations that can leave the width of a .NET sized integer are routed diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index a2a6a1bd35..0c81264c1d 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -38,6 +38,31 @@ let private isFunctionType (t: Type) = | DelegateType _ -> true | _ -> false +let private printfArgumentTypes (t: Type) = + match t.Generics |> List.tryHead with + | Some(Fable.Transforms.AST.NestedLambdaType(argTypes, _)) -> argTypes + | _ -> [] + +let private printfFormatters (com: ICompiler) r (t: Type) = + let formatters, hasCustomFormatter = + (([], false), printfArgumentTypes t) + ||> List.fold (fun (formatters, hasCustomFormatter) argType -> + let arg = makeTypedIdent argType $"printf_arg_%d{com.IncrementCounter()}" + + match ObjectOverrides.tryCallZeroArg com r String "ToString" (IdentExpr arg) with + | Some call -> Lambda(arg, call, None) :: formatters, true + | None -> Value(Null Any, None) :: formatters, hasCustomFormatter + ) + + if hasCustomFormatter then + // Printf applies values later through a curried runtime function. Preserve the static type + // knowledge as a parallel list of converters; `undefined` keeps the ordinary runtime path. + (Value(NewList(None, Any), None), formatters) + ||> List.fold (fun tail formatter -> Value(NewList(Some(formatter, tail), Any), None)) + |> Some + else + None + let private physicalEquals r (left: Expr) (right: Expr) = // Reference identity. For function values, the same F# value can be represented as either an // uncurried fun or a re-curried nested adapter at different sites, which Erlang's `=:=` sees as @@ -206,7 +231,12 @@ let private fsFormat Helper.LibCall(com, "fable_string", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r) |> Some | ".ctor", _, arg :: _ -> - Helper.LibCall(com, "fable_string", "printf", t, [ arg ], i.SignatureArgTypes, ?loc = r) + let printfArgs = + match printfFormatters com r t with + | Some formatters -> [ arg; formatters ] + | None -> [ arg ] + + Helper.LibCall(com, "fable_string", "printf", t, printfArgs, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None @@ -741,36 +771,7 @@ let private objects Helper.LibCall(com, "fable_comparison", "hash", t, [ thisObj ], ?loc = r) |> Some | "GetType", Some arg, _ -> makeTypeInfo r arg.Type |> Some - | "ToString", Some thisObj, [] -> - match thisObj.Type with - | Type.Char -> emitExpr r t [ thisObj ] "<<($0)/utf8>>" |> Some - | Type.Number(kind, _) -> - match kind with - | Decimal -> - Helper.LibCall(com, "fable_decimal", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | Float16 - | Float32 - | Float64 -> - Helper.LibCall(com, "fable_convert", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | _ -> emitExpr r t [ thisObj ] "integer_to_binary($0)" |> Some - | Type.Boolean -> emitExpr r t [ thisObj ] "atom_to_binary($0)" |> Some - | Type.String -> Some thisObj - | DeclaredType(ent, _) when ent.FullName = Types.timespan -> - Helper.LibCall(com, "fable_timespan", "to_string", t, [ thisObj ], ?loc = r) - |> Some - | DeclaredType(ent, _) when ent.FullName = Types.datetime -> - Helper.LibCall(com, "fable_date", "to_string", t, [ thisObj ], ?loc = r) |> Some - | DeclaredType(ent, _) when ent.FullName = "System.Uri" -> - Helper.LibCall(com, "fable_uri", "to_string", t, [ thisObj ], ?loc = r) |> Some - | DeclaredType(ent, _) when ent.FullName = "System.Text.StringBuilder" -> - // StringBuilder.ToString() → iolist_to_binary(get(maps:get(field_buf, get(Sb)))) - emitExpr r t [ thisObj ] "iolist_to_binary(get(maps:get(field_buf, get($0))))" - |> Some - | _ -> - Helper.LibCall(com, "fable_convert", "to_string", t, [ thisObj ], ?loc = r) - |> Some + | "ToString", Some thisObj, [] -> ToString.toStringByType com r t thisObj | _ -> None /// Beam-specific System.ValueType replacements. diff --git a/src/fable-library-beam/fable_string.erl b/src/fable-library-beam/fable_string.erl index b639de5706..a9e8ee0b29 100644 --- a/src/fable-library-beam/fable_string.erl +++ b/src/fable-library-beam/fable_string.erl @@ -39,7 +39,7 @@ split_remove_empty/2, split_with_count/3, to_string/1, - printf/1, + printf/1, printf/2, to_text/1, to_text/2, to_text/3, to_text/4, to_text/5, to_console/1, to_console/2, to_console/3, to_console/4, to_console/5, to_console_error/1, to_console_error/2, to_console_error/3, @@ -103,6 +103,7 @@ -spec split_with_count(binary(), binary() | integer() | list(), non_neg_integer()) -> list(). -spec to_string(term()) -> binary(). -spec printf(binary()) -> map(). +-spec printf(binary(), list()) -> map(). -spec to_text(map() | binary()) -> binary() | fun(). -spec to_text(map() | binary(), term()) -> binary() | fun(). -spec to_text(map() | binary(), term(), term()) -> binary() | fun(). @@ -438,10 +439,14 @@ to_string(V) -> %% Printf / sprintf / String.Format support %% ===================================================================== -%% printf/1 — Parse an F# format string and return a format object (map). +%% printf/1,2 — Parse an F# format string and return a format object (map). +%% The formatter list preserves statically known custom ToString converters for %O arguments. %% The format object has: input (original string), cont (function accepting a continuation). printf(Str) -> - Printer = fs_format(Str), + printf(Str, []). + +printf(Str, Formatters) -> + Printer = fs_format(Str, Formatters), #{input => Str, cont => Printer}. %% apply_curried/2 — Apply a list of args one at a time to a curried function. @@ -801,11 +806,11 @@ pad_zeros(Bin, Width) -> %% Internal: F# format string parser %% ===================================================================== -%% fs_format/1 — Parse F# format string like "%d %s %.2f" and return +%% fs_format/2 — Parse F# format string like "%d %s %.2f" and return %% fun(Cont) -> fun(Arg1) -> fun(Arg2) -> ... -> Cont(Result) end end end -fs_format(Str) -> +fs_format(Str, Formatters) -> {Parts, Specs} = parse_format(Str, <<>>, [], []), - create_printer(Parts, Specs). + create_printer(Parts, Specs, Formatters). %% parse_format/4 — Split format string into literal parts and format specifiers. %% Returns {[binary()], [string()]} where Parts has one more element than Specs. @@ -845,30 +850,43 @@ parse_spec_precision(<>, Acc) when C >= $0, C =< $9 -> parse_spec_precision(<>, Acc) -> {[C | Acc], Rest}. -%% create_printer/2 — Build nested curried functions from parts and specifiers. +%% create_printer/3 — Build nested curried functions from parts and specifiers. %% Parts = [P0, P1, P2, ...], Specs = [S1, S2, ...] %% Result: fun(Cont) -> if no specs, Cont(P0); else fun(Arg1) -> ... end end -create_printer([SinglePart], []) -> +create_printer([SinglePart], [], _Formatters) -> %% No format specifiers — just a plain string fun(Cont) -> Cont(SinglePart) end; -create_printer(Parts, Specs) -> +create_printer(Parts, Specs, Formatters) -> fun(Cont) -> - build_curried(Parts, Specs, Cont, <<>>) + build_curried(Parts, Specs, Formatters, Cont, <<>>) end. -build_curried([Part], [], Cont, Acc) -> +build_curried([Part], [], _Formatters, Cont, Acc) -> Cont(<>); -build_curried([Part | RestParts], [Spec | RestSpecs], Cont, Acc) -> +build_curried([Part | RestParts], [Spec | RestSpecs], [Formatter | RestFormatters], Cont, Acc) -> + NewAcc = <>, + fun(Arg) -> + Formatted = format_value(Spec, Arg, Formatter), + build_curried(RestParts, RestSpecs, RestFormatters, Cont, <>) + end; +build_curried([Part | RestParts], [Spec | RestSpecs], [], Cont, Acc) -> NewAcc = <>, fun(Arg) -> - Formatted = format_value(Spec, Arg), - build_curried(RestParts, RestSpecs, Cont, <>) + Formatted = format_value(Spec, Arg, undefined), + build_curried(RestParts, RestSpecs, [], Cont, <>) end. -%% format_value/2 — Format a single value according to a format specifier string. +%% format_value/2,3 — Format a single value according to a format specifier string. format_value(Spec, Value) -> + format_value(Spec, Value, undefined). + +format_value(Spec, Value, Formatter) -> {Flags, Width, Prec, Type} = parse_spec_parts(Spec), - Raw = format_raw(Type, Prec, Value), + Raw = + case Type =:= $O andalso is_function(Formatter, 1) of + true -> Formatter(Value); + false -> format_raw(Type, Prec, Value) + end, WithSign = apply_sign_flag(Flags, Raw, Value), apply_width(Flags, Width, WithSign). diff --git a/tests/Beam/RecordTests.fs b/tests/Beam/RecordTests.fs index 685e89ee6c..fe296cfdbd 100644 --- a/tests/Beam/RecordTests.fs +++ b/tests/Beam/RecordTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.RecordTests open Fable.Tests.Util +open Fable.Tests.UnionTypes open Util.Testing type Person = { Name: string; Age: int } @@ -41,6 +42,11 @@ type RecordWithProperty = { items: string list } member this.fullName = String.concat " - " this.items +type IntegrityRecord = + { Level: string } + + override this.ToString() = this.Level + [] let ``test simple record creation works`` () = let p = { Name = "Alice"; Age = 30 } @@ -181,6 +187,24 @@ let ``test Record property access uses correct naming`` () = let x = { items = ["Hello"; "World"] } equal "Hello - World" x.fullName +[] +let ``test record ToString override works`` () = + let value: IntegrityRecord = { Level = "Untrusted" } + value.ToString() |> equal "Untrusted" + string value |> equal "Untrusted" + +[] +let ``test percent O uses record ToString override`` () = + let value: IntegrityRecord = { Level = "Untrusted" } + sprintf "%O" value |> equal "Untrusted" + +[] +let ``test ToString override can be called across files`` () = + let value = IntegrityLevel.Trusted + value.ToString() |> equal "Trusted" + string value |> equal "Trusted" + sprintf "%O" value |> equal "Trusted" + [] let ``test Anonymous records can have optional fields`` () = let add (o: {| bar: int option; zas: string option; foo: int option option |}) = diff --git a/tests/Beam/UnionTypeTests.fs b/tests/Beam/UnionTypeTests.fs index c0d71a8dc5..a104330bd9 100644 --- a/tests/Beam/UnionTypeTests.fs +++ b/tests/Beam/UnionTypeTests.fs @@ -24,6 +24,15 @@ type MyUnion2 = | Tag of string | NewTag of string +type IntegrityLevel = + | Untrusted + | Trusted + + override this.ToString() = + match this with + | Untrusted -> "Untrusted" + | Trusted -> "Trusted" + type IntUnion = | IntCase1 of int | IntCase2 of int @@ -84,6 +93,20 @@ let ``test Union cases matches with no arguments can be generated`` () = | Male -> false |> equal false +[] +let ``test union ToString override works`` () = + let value = IntegrityLevel.Untrusted + value.ToString() |> equal "Untrusted" + string value |> equal "Untrusted" + +[] +let ``test percent O uses union ToString override`` () = + sprintf "%O" IntegrityLevel.Untrusted |> equal "Untrusted" + sprintf "%d %O" 42 IntegrityLevel.Untrusted |> equal "42 Untrusted" + + let formatIntegrityLevel = sprintf "%O" + formatIntegrityLevel IntegrityLevel.Trusted |> equal "Trusted" + [] let ``test Union cases matches with one argument can be generated`` () = let x = Left "abc" From e2120e1084a259d4080a5943d209b1ddfc400f01 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 27 Sep 2026 20:36:17 +0200 Subject: [PATCH 099/322] fix(beam): support AggregateException (#4999) --- src/Fable.Transforms/Beam/Replacements.fs | 18 +++++++ src/fable-library-beam/fable_utils.erl | 25 +++++++++ tests/Beam/Fable.Tests.Beam.fsproj | 1 + tests/Beam/MiscTests.fs | 62 +++++++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 0c81264c1d..078f4107b8 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5934,6 +5934,24 @@ let tryCall | "Capture", _, [ arg ] -> Some arg | "Throw", Some arg, _ -> makeThrow r t arg |> Some | _ -> None + | "System.AggregateException" -> + let makeAggregateException message exceptions = + Helper.LibCall(com, "fable_utils", "aggregate_exception", t, [ message; exceptions ], ?loc = r) + |> Some + + match info.CompiledName, thisArg, args, info.SignatureArgTypes with + | ".ctor", None, [], _ -> + makeAggregateException (makeStrConst "One or more errors occurred.") (Value(NewList(None, Any), None)) + | ".ctor", None, [ message ], [ String ] -> makeAggregateException message (Value(NewList(None, Any), None)) + | ".ctor", None, [ exceptions ], [ _ ] -> + makeAggregateException (makeStrConst "One or more errors occurred.") exceptions + | ".ctor", None, [ message; exceptions ], [ String; _ ] -> makeAggregateException message exceptions + | "get_Message", Some aggregate, _, _ -> emitExpr r t [ aggregate ] "maps:get(message, $0, $0)" |> Some + | "get_InnerException", Some aggregate, _, _ -> + emitExpr r t [ aggregate ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_InnerExceptions", Some aggregate, _, _ -> + emitExpr r t [ aggregate ] "maps:get(inner_exceptions, $0)" |> Some + | _ -> None | "System.Exception" -> match info.CompiledName, thisArg, args with | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index 0f2ac63b7e..6f13d5af3c 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -24,6 +24,7 @@ using/2, to_list/1, enumerate_to_list/1, + aggregate_exception/2, new_byte_array/1, new_byte_array_zeroed/1, new_byte_array_filled/2, @@ -63,6 +64,7 @@ -spec using(term(), fun()) -> term(). -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). +-spec aggregate_exception(binary(), term()) -> map(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). -spec new_byte_array_zeroed(non_neg_integer()) -> tuple(). -spec new_byte_array_filled(non_neg_integer(), non_neg_integer()) -> tuple(). @@ -451,6 +453,29 @@ to_list(V) when is_binary(V) -> to_list(V) -> V. +%% AggregateException keeps the normal BEAM exception-map representation. Its exception +%% sequence may be a params array ref, a list, or any other Fable enumerable. +aggregate_exception(BaseMessage, ExceptionsValue) -> + Exceptions = to_list(ExceptionsValue), + InnerException = + case Exceptions of + [First | _] -> First; + [] -> undefined + end, + Message = iolist_to_binary([ + BaseMessage + | [[<<" (">>, exception_message(Exception), <<")">>] || Exception <- Exceptions] + ]), + #{ + message => Message, + inner_exception => InnerException, + inner_exceptions => new_ref(Exceptions) + }. + +exception_message(#{message := Message}) -> Message; +exception_message(Exception) when is_reference(Exception) -> exception_message(get(Exception)); +exception_message(Exception) -> format_term(Exception). + %% Enumerate a lazy seq object to a plain list using get_enumerator/move_next/get_current. enumerate_to_list(Seq) -> E = get_enumerator(Seq), diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index f8344cb3ff..9cdcbf32ed 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -18,6 +18,7 @@ + diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index f57f446add..4168846f65 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -746,6 +746,68 @@ let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true +[] +let ``test AggregateException retains all supplied exceptions`` () = + let first = Exception("first") + let second = Exception("second") + let aggregate = AggregateException(first, second) + + aggregate.Message |> equal "One or more errors occurred. (first) (second)" + aggregate.InnerException.Message |> equal "first" + + aggregate.InnerExceptions + |> Seq.map (fun ex -> ex.Message) + |> Seq.toList + |> equal [ "first"; "second" ] + +[] +let ``test AggregateException practical constructors work`` () = + let first = Exception("first") + let second = Exception("second") + let empty = AggregateException() + let messageOnly = AggregateException("explicit") + let exceptions = AggregateException([| first; second |]) + let messageAndExceptions = AggregateException("explicit", [| first; second |]) + + empty.Message |> equal "One or more errors occurred." + empty.InnerExceptions |> Seq.isEmpty |> equal true + messageOnly.Message |> equal "explicit" + messageOnly.InnerExceptions |> Seq.isEmpty |> equal true + exceptions.InnerException.Message |> equal "first" + messageAndExceptions.Message |> equal "explicit (first) (second)" + +[] +let ``test raised AggregateException retains its details when caught`` () = + let first = Exception("first") + let second = Exception("second") + let aggregate = AggregateException("explicit", [| first; second |]) + + let caughtMessage, caughtInnerMessages = + try + raise aggregate + failwith "unreachable" + with caught -> + let caught = unbox caught + + caught.Message, + (caught.InnerExceptions + |> Seq.map (fun ex -> ex.Message) + |> Seq.toList) + + caughtMessage |> equal "explicit (first) (second)" + caughtInnerMessages |> equal [ "first"; "second" ] + +[] +let ``test ordinary Exception and ArgumentException retain inner exceptions`` () = + let inner = Exception("inner") + let plain = Exception("plain", inner) + let argument = ArgumentException("argument", inner) + + plain.Message |> equal "plain" + plain.InnerException.Message |> equal "inner" + argument.Message |> equal "argument" + argument.InnerException.Message |> equal "inner" + // -- General / Misc -- [] From 0e9329ddf6886a5c34198d18f142f9d2a352f54c Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 08:56:34 +0200 Subject: [PATCH 100/322] docs(beam): rewrite target reference and roadmap (#5000) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 2066 ++++++----------------- tests/Beam/ConversionTests.fs | 11 +- 2 files changed, 484 insertions(+), 1593 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 2da0eb65b5..4df00964d1 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -1,1670 +1,562 @@ -# Fable.Beam — F# on the BEAM +# Fable.Beam — F# to Erlang -An Erlang/BEAM target for Fable. +Fable.Beam is Fable's Erlang/BEAM target. It compiles F# to readable `.erl` +source and supplies an Erlang runtime for F# and .NET library semantics. -## Motivation - -- Learn the BEAM/OTP platform deeply by building a compiler target (the same - approach that made Fable.Python a success for learning Python) -- Bring F#'s type system, pattern matching, and computation expressions to the - BEAM ecosystem -- Bring F#'s `MailboxProcessor` to the BEAM as an in-process, source-compatible - abstraction (so existing F# async/agent code just compiles) — note this is a - same-process CPS model, **not** an OTP actor; `MailboxProcessor` turned out not to be - the right surface for real process-isolated OTP actors -- Provide real OTP concurrency (process-isolated actors, supervision trees, hot code - reloading) through the separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) - bindings library and the [Fable.Actor](https://github.com/fable-hub/Fable.Actor) model, - rather than overloading `MailboxProcessor` +| Item | Value | +| --- | --- | +| CLI | `dotnet fable --lang beam` (`--lang erlang` is an alias) | +| Output | Erlang source plus a rebar3 application scaffold | +| Minimum runtime | Erlang/OTP 25 | +| Target status | Alpha | +| Full validation | `./build.sh test beam` | -## Minimum OTP Version: 25 +OTP 25 is the support floor. The generated code also relies on maps and named +funs, `monotonic_time`/`system_time`, `atomics`, and `uri_string`; all are +available by that release. -Fable.Beam requires **OTP 25 or later**. This is the oldest version still available -via `apt install erlang` on current Linux LTS distributions (Ubuntu 22.04, Debian 12, -Pop!_OS). Users should be able to install Erlang from their distro package manager -and have it work without adding third-party repos. +The compiler target is responsible for correct Erlang output. OTP bindings and +process-based application models are separate: -Key OTP features the runtime depends on: +- [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) provides typed OTP + bindings. +- [Fable.Actor](https://github.com/fable-hub/Fable.Actor) provides an actor model. +- `MailboxProcessor`, `Async`, and `Task` provide F#-compatible APIs; they are not + replacements for OTP behaviours. `Task` deliberately shares the cold `Async` + runtime, so evaluating a task does not start it as a .NET hot task would. -| Feature | Minimum OTP | Used In | -| --- | --- | --- | -| Maps (`#{}`) | 17 | All modules | -| Named funs (`fun F(X) -> ...`) | 17 | Generated recursive lambdas | -| `erlang:monotonic_time/1` | 18 | `fable_stopwatch.erl` | -| `erlang:system_time/1` | 18 | `fable_date.erl` | -| `atomics` module | 21 | `fable_utils.erl` (byte arrays), `fable_random.erl` | -| `uri_string` module | 21 | `fable_uri.erl` | -| JIT compiler | 24 (improved in 25) | Bit-syntax integer wrapping performance | -| `maybe` keyword reserved | 25 | Escaped in `sanitizeErlangName` | +A known private consumer is an application of about 92,000 lines of F# that uses +file and network I/O, OTP supervision trees, actors, long-running services, and +third-party packages. This is evidence that the target can support substantial +applications; the alpha status reflects the semantic gaps documented below. -## Target Language: Erlang Source +## Compiler pipeline -Generate `.erl` files (not Core Erlang, not Elixir). Rationale: +```text +F# source + -> FSharp2Fable + -> Fable AST + -> shared Fable transforms + -> Fable2Beam + -> Erlang AST + -> ErlangPrinter + -> .erl source + -> rebar3 / Erlang compiler +``` -- **Learning-first**: reading generated Erlang output teaches the language -- **OTP integration**: OTP docs and patterns are written in Erlang -- **Debuggable**: users can read and understand the output -- **Sufficient**: Erlang surface syntax is regular enough for code generation -- Can always add a Core Erlang backend later if performance demands it +| Path | Responsibility | +| --- | --- | +| `Beam.AST.fs` | Minimal Erlang AST | +| `Fable2Beam.fs` | Fable AST to Erlang AST | +| `Fable2Beam.Util.fs` | Shared lowering helpers | +| `Fable2Beam.Reflection.fs` | Compile-time reflection metadata | +| `Replacements.fs` | .NET and FSharp.Core calls to Erlang/runtime calls | +| `ErlangPrinter.fs` | Erlang source generation | +| `Prelude.fs` | Names, keywords, module identities, and OTP collision checks | +| `src/fable-library-beam/` | Erlang and F# runtime library | +| `src/Fable.Build/Test/Beam.fs` | .NET, generated Erlang, and entry-point validation | +| `tests/Beam/` | Target test suite and native Erlang fixtures | + +The Erlang AST contains only the forms needed by the target: literals, patterns, +tuples, lists, maps, calls, functions, `case`, matches, blocks, operators, +`try`/`catch`, `receive`, and `Emit`. Target-specific syntax that does not justify +a dedicated node uses `Emit`. + +## Design rules + +- Prefer Erlang/OTP primitives when they preserve F# semantics. +- Handle target-specific calls in `Beam/Replacements.fs` before the JavaScript + replacement fallback can add JavaScript-only helpers or arguments. +- Keep Fable AST to Erlang AST lowering in the transform files; put reusable + behavior in utilities or the runtime. +- Treat the F#/.NET tests as the semantic contract. Do not weaken a passing .NET + test to accommodate the target. +- Keep generated Erlang readable and compatible with ordinary Erlang tooling. +- Keep OTP behaviours out of the compiler. They belong in typed bindings and + libraries built on the generated modules. + +Common native mappings: + +| F# operation | Erlang implementation | +| --- | --- | +| Integer arithmetic | Native arbitrary-precision arithmetic plus width wrapping | +| Bitwise operations | `band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot` | +| Structural equality | `=:=` | +| Lists | Native linked lists and `lists` | +| Records and maps | Erlang maps and `maps` | +| Sets | `ordsets` plus `fable_set` | +| Hashing | `erlang:phash2/1` | +| Pattern matching | Erlang `case` clauses and patterns | -## Architecture +## Modules and generated projects -Same pipeline as all Fable targets: +Erlang has one flat, global module namespace. A generated module name therefore +contains its OTP application name and source path. -```text -F# Source - ↓ FSharp2Fable (existing) -Fable AST - ↓ FableTransforms (existing) -Fable AST (optimized) - ↓ Fable2Beam -Erlang AST - ↓ ErlangPrinter -.erl source files -``` - -## Design Principles: Beam as an Independent Target +| F# source | Erlang module | +| --- | --- | +| `Program.fs` in `MyApp` | `my_app_program` | +| `Misc/Util2.fs` in `MyApp` | `my_app_misc_util2` | +| `DSL.fs` in `Scriptorium.Quill` | `scriptorium_quill_dsl` | +| `fable_modules/Hedgehog.0.11/Gen.fs` | `hedgehog_gen` | -Fable.Beam should be its own target with its own idioms. While the compiler pipeline -is shared with other targets, the **Replacements layer and runtime library should -take full advantage of Erlang/BEAM capabilities** rather than inheriting patterns -from JavaScript or Python that don't fit. +`Fable.Beam.Naming.erlangModuleName` is the single naming implementation used by +code generation and output-path generation. The CLI rejects: -### Don't inherit JS/Python patterns when Erlang is simpler +- two sources that resolve to the same module atom; +- generated names that collide with modules from `erts`, `kernel`, or `stdlib`. -Many .NET BCL operations that require complex library emulation in JS or Python map -directly to Erlang built-ins: +Exceptions: -| Area | JS/Python approach | Erlang approach | -| --- | --- | --- | -| **Integers** | Fixed-width emulation (JS BigInt, Python PyO3 Rust) | Native arbitrary-precision — direct `+`, `-`, `*`, `div`, `rem` | -| **Int64/BigInt** | Library calls (`big_int:op_add`, etc.) | Direct binary ops — Erlang integers ARE arbitrary-precision | -| **Bitwise ops** | JS routes Int64 through BigInt library | Native `band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot` | -| **Lists** | Array-based emulation | Native linked lists — direct `[H\|T]`, `lists:*` | -| **Maps** | Library objects/dicts | Native `#{}` maps, `maps:*` | -| **Sets** | Library Set class | Native `ordsets` (sorted lists) | -| **Structural equality** | `Util.equals()` library call | Native `=:=` (deep comparison on all types) | -| **Structural comparison** | `Util.compare()` library call | Native `<`, `>`, `=<`, `>=` for most types; `fable_comparison:compare_union/3` for DUs (see below) | -| **Hashing** | Custom hash functions | `erlang:phash2/1` | -| **Pattern matching** | Compiled to if/else chains | Native pattern matching in `case` expressions | -| **Sequences** | Lazy iterators | Lazy seqs via compiled `seq.erl`/`seq2.erl` | - -**Rule: If Erlang can do it natively, do it natively.** Only create library modules -(`fable-library-beam/*.erl`) for operations that genuinely need helper code. Avoid -falling through to the JS Replacements fallback for Beam-specific operations. - -#### Exception: ordered comparison of discriminated unions - -F# requires `<`, `<=`, `>`, `>=`, `compare`, sorting, `min`/`max` on a DU to order cases -by **declaration order** (the case tag index). The Beam DU representation is tagless — -nullary cases are bare atoms (`error`, `warning`, …) and other cases are `{atom, Field…}` -tuples — so Erlang's native term ordering falls back to **alphabetical atom order**, which -does not match F# semantics (e.g. `Warning <= Info` is `true` in F# but `false` for atoms). - -Since a value carries no case index, the compiler supplies the declaration order at the -comparison site and routes DU comparisons through `fable_comparison:compare_union(TagOrder, A, B)`, -where `TagOrder` is the list of case tag names in declaration order. This is wired up in -`Beam/Replacements.fs`: - -- `compare` helper → `compare_union` when the operand type is an F# union (covers `compare`, - `GenericComparison`, `CompareTo`, and the comparer used by `List.sortWith` when the user - passes `compare`). Note this does **not** reach `Set`/`Map`, which use native `ordsets`/`#{}` - and never call `fable_comparison` — see the gaps below. -- `makeRelational` → relational operators on unions. -- `min`/`max` operators on unions (via the union-aware `compare` helper). -- `List.sort`/`List.sortDescending` and `Array.sort`/`Array.sortDescending` → an inline - `lists:sort/2` with a `compare_union` comparator when the element type is a union. - -Equality (`=:=`) is already correct for unions and is left untouched. - -**Known gaps (routing is site-driven, so only statically-typed union comparisons are covered):** -comparisons that reach the *generic runtime* `fable_comparison:compare/2` — or Erlang's native -term ordering — on a union value still use alphabetical order. This affects: - -- `List.sortBy`/`sortByDescending` with a **union key**. -- `List.min`/`max` and `Array.min`/`max` (native `lists:min`/`lists:max`). -- A union used as a **field of another union** (nested comparison when outer tags are equal). -- **`Set` and `Map`** — F# Sets/Maps are ordered collections, but the Beam - representation is native `ordsets`/`#{}`, which order by native term ordering and never call - `fable_comparison`. So `Set.minElement`/`maxElement`, `Set`/`Map` iteration order, - `Map.toList`/`Keys`, etc. sort union elements/keys **alphabetically**, not by declaration - order. (Set/Map *membership* and *equality* are unaffected — those rely on `=:=`, which is - correct.) - -These are not currently exercised by the test suite. - -**Possible future follow-up — make the representation carry the tag index.** The complete, -uniform fix is to encode the case's declaration index in the value itself (e.g. an -index-first tuple `{Index, atom, Field…}`, nullary → `{Index, atom}`). Then the *generic* -`fable_comparison:compare/2` orders correctly for **every** path — relational ops, `sortBy`, -`min`/`max`, nested unions, generic containers — with no per-site tag lists and no gaps. -Why it was **not** done here: (1) the tagless bare-atom / `{atom, Field…}` shape is a -deliberate, idiomatic-Erlang design goal and is FFI-exposed (DU values flow through -`Erlang.receive` message-passing and hand-written interop); (2) Erlang orders tuples by -**size first**, so even an index-carrying tuple can't rely on native `=<`/`lists:sort` for -mixed-arity unions — union comparison would still have to route through `fable_comparison`; -(3) the blast radius is broad and must move in lockstep — `NewUnion`, `UnionCaseTest`, -`UnionTag`, `UnionField` (offset), `Erlang.receive`, `fable_reflection` (make/get union), -the `Result` shape (`{ok, V}` → `{Index, ok, V}`), and `%A`/`~p` printed output. If the gaps -above start to matter in real code, revisit this representation change. - -### Never modify F# tests to accommodate Erlang quirks - -The F# test suite represents valid F# code that must compile and run correctly on all -targets. When an Erlang edge case causes a test failure: - -- **DO**: Add a helper function in `fable-library-beam/` that handles the edge case - (e.g., `fable_convert:to_float/1` handles `"1."` which `binary_to_float/1` rejects) -- **DO**: Use `#if FABLE_COMPILER` blocks for genuine cross-platform differences - (e.g., .NET CultureInfo in parsing) -- **DON'T**: Change the F# test input to avoid the edge case (e.g., changing - `float("1.")` to `float("1.0")` — this hides the bug) - -### Intercept in Beam Replacements, not JS fallback - -The Replacements pipeline tries `Beam.Replacements.tryCall` first, then falls back to -`JS.Replacements.tryCall`. The JS fallback injects extra arguments (comparers, adders) -that Erlang doesn't need and generates library imports (`Util`, `BigInt`, etc.) that -don't exist in Erlang. - -**Handle operations in Beam Replacements** to get clean, original argument lists. -Reserve the JS fallback only for operations that genuinely work the same way. - -## New Files - -### Compiler transforms (`src/Fable.Transforms/Beam/`) - -| File | Purpose | Reference | Status | -| -------------------------- | --------------------------------------------------- | ----------------------------- | ------ | -| `Beam.AST.fs` | Erlang AST type definitions (intentionally minimal) | `Python/Python.AST.fs` | Done | -| `Fable2Beam.fs` | Main Fable AST → Erlang AST transforms | `Fable2Php.fs` / Python | Done | -| `Fable2Beam.Util.fs` | Shared helpers for the transforms | `Python/Fable2Python.Util.fs` | Done | -| `Fable2Beam.Reflection.fs` | Compile-time reflection type-info generation | Python reflection | Done | -| `Replacements.fs` | .NET BCL → Erlang mappings (full implementation) | `Python/Replacements.fs` | Done | -| `ErlangPrinter.fs` | Erlang AST → `.erl` source code | `Python/PythonPrinter.fs` | Done | -| `Prelude.fs` | Name sanitization + Erlang keyword escaping | — | Done | - -Started as a single `Fable2Beam.fs` (PHP pattern) and has since split out -`Fable2Beam.Util.fs` and `Fable2Beam.Reflection.fs` as complexity grew (Python pattern). -The Erlang AST (`Beam.AST.fs`) deliberately stayed small — see "Erlang AST" below. - -### Runtime library (`src/fable-library-beam/`) - -Erlang modules implementing F# core types: - -| Module | Purpose | Notes | Status | -| --- | --- | --- | --- | -| fable_option.erl | Option | Some(x) = x, None = undefined | Done | -| fable_list.erl | FSharpList | fold, find, choose, collect, etc. | Done | -| fable_map.erl | FSharpMap | Erlang native maps, pick/try_pick/min/max | Done | -| fable_seq.erl | Seq / IEnumerable | Eager lists, delay/singleton/unfold | Done | -| fable_string.erl | String utilities | Erlang binaries, pad/replace/join, sprintf/printf/String.Format | Done | -| fable_comparison.erl | Comparison | compare/2 returning -1/0/1 | Done | -| fable_char.erl | Char utilities | is_letter/digit/upper/lower/whitespace | Done | -| fable_convert.erl | Type conversions | Robust to_float handling edge cases | Done | -| fable_reflection.erl | Reflection | Full FSharpType/FSharpValue support: TypeInfo as maps, record/union/tuple/function type tests, GetRecordFields/MakeRecord, GetUnionFields/MakeUnion, GetTupleFields/MakeTuple, PropertyInfo.GetValue | Done | -| fable_result.erl | Result | {ok, V} or {error, E} | Done | -| fable_set.erl | FSharpSet | ordsets (sorted lists), fold/map/filter/partition/union_many/intersect_many | Done | -| fable_async_builder.erl | AsyncBuilder | CPS builder operations (bind, return, delay, etc.) | Done | -| fable_async.erl | Async | High-level ops (RunSynchronously, Parallel, Sleep, etc.) | Done | -| fable_regex.erl | Regex | Wraps Erlang `re` module (PCRE), IsMatch/Match/Matches/Replace/Split | Done | -| fable_resize_array.erl | ResizeArray | List manipulation helpers (set_item, remove, insert, find, sort) | Done | -| fable_dictionary.erl | Dictionary | Mutable dictionary via process dict + Erlang maps, TryGetValue with out-refs | Done | -| fable_hashset.erl | HashSet | Mutable set via process dict + Erlang maps, UnionWith/IntersectWith/ExceptWith | Done | -| fable_queue.erl | Queue | FIFO queue via process dict + Erlang queue module | Done | -| fable_stack.erl | Stack | LIFO stack via process dict + list | Done | -| fable_timespan.erl | TimeSpan | Ticks-based, create/from_*/component accessors/total_*/arithmetic/parse/to_string | Done | -| fable_date.erl | DateTime | 2-tuple {Ticks, Kind}, calendar module, formatting, parsing, arithmetic | Done | -| fable_date_offset.erl | DateTimeOffset | 3-tuple {Ticks, OffsetTicks, Kind}, wraps fable_date | Done | -| fable_guid.erl | Guid | UUID v4 generation, parse, toString, comparison | Done | -| fable_uri.erl | Uri | URI parsing and manipulation | Done | -| fable_utils.erl | Utilities | IEnumerator (lists/refs/maps/HashSet), apply_curried, infinity/NaN helpers, atomics byte arrays (new_byte_array, byte_array_get/set/length) | Done | -| fable_bit_converter.erl | BitConverter | Byte conversion, endianness | Done | -| fable_decimal.erl | Decimal | Fixed-scale integer (value × 10^28), multiply/divide/to_string/parse/from_parts | Done | -| fable_mailbox.erl | MailboxProcessor | In-process CPS continuation model (same as JS/Python) | Done | -| fable_cancellation.erl | CancellationToken | Process dict pattern, create/cancel/register/is_cancellation_requested, timer-based auto-cancel | Done | -| fable_stopwatch.erl | Stopwatch | StartNew, Elapsed, ElapsedMilliseconds, Stop, Reset, Restart, IsRunning, Frequency, GetTimestamp | Done | -| fable_observable.erl | Observable | subscribe, add, choose, filter, map, merge, pairwise, partition, scan, split | Done | -| fable_event.erl | Event / IEvent | trigger/publish/add, choose/filter/map/merge/pairwise/partition/scan/split | Done | -| fable_date_only.erl | DateOnly | create/components/day_number/add_*/from_date_time/to_string/parse | Done | -| fable_time_only.erl | TimeOnly | create/from_*/components/ticks/add_*/is_between/to_time_span/to_string/parse | Done | -| fable_parallel.erl | Array.Parallel | spawn-based parallel_map/mapi/init/iter/iteri/collect/choose/for | Done | -| fable_quotation.erl | F# Quotations (Expr) | `mk_*` constructors, `is_*` tests, evaluate, substitute, get_free_vars | Done | - -### Registration & CLI (modified existing files) -- All Done - -| File | Change | Status | -| ---------------------------------------------- | ----------------------------------------------- | ------ | -| `src/Fable.AST/Plugins.fs` | Added `Beam` to `Language` DU | Done | -| `src/Fable.Compiler/Util.fs` | Added `.erl` file extension | Done | -| `src/Fable.Compiler/ProjectCracker.fs` | Added Beam library path | Done | -| `src/Fable.Cli/Entry.fs` | Added `--lang beam` argument parsing | Done | -| `src/Fable.Cli/Pipeline.fs` | Added `Beam.compileFile` dispatch | Done | -| `src/Fable.Transforms/Replacements.Api.fs` | Beam dispatch for all 15 API functions | Done | -| `src/Fable.Transforms/Transforms.Util.fs` | Added Beam to `getLibPath` | Done | -| `src/Fable.Transforms/FSharp2Fable.Util.fs` | Added Beam to `isModuleValueCompiledAsFunction` | Done | -| `src/Fable.Transforms/Fable.Transforms.fsproj` | Added Beam files to project | Done | - -### Build system (`src/Fable.Build/`) - -| File | Change | Status | -| ---------------------- | ------------------------------------------------ | ------ | -| `FableLibrary/Beam.fs` | `BuildFableLibraryBeam` class | Done | -| `Quicktest/Beam.fs` | Quicktest handler for beam | Done | -| `Test/Beam.fs` | Test handler (`./build.sh test beam`) | Done | -| `Main.fs` | Added beam to quicktest + test + fable-library | Done | -| `Fable.Build.fsproj` | Added new Beam files | Done | - -### Quicktest project (`src/quicktest-beam/`) - -| File | Purpose | Status | -| --------------------- | ----------------------------------- | ------ | -| `quicktest.fs` | `printfn "Hello from BEAM!"` | Done | -| `quicktest.fsproj` | Project file referencing Fable.Core | Done | - -## Module Naming - -Erlang's module namespace is **flat and global**. The atom in `-module(...)` is a module's only -identity: neither the directory the `.erl` file sits in nor the OTP application it belongs to -scopes it, and the code server resolves the atom across the whole code path. An `.erl` file must -also be named after the module it declares. - -So every generated module name is qualified by the application it belongs to — the same -convention OTP itself follows (`cowboy_req`, `rebar_app_info`) and that Fable's own runtime -already used (`fable_list`, `fable_map`): - -| F# source | Erlang module | -| ------------------------------------ | ----------------------- | -| `/Program.fs` in `MyApp` | `my_app_program` | -| `/Misc/Util2.fs` in `MyApp` | `my_app_misc_util2` | -| `../Scriptorium.Quill/DSL.fs` | `scriptorium_quill_dsl` | -| `fable_modules/Hedgehog.0.11/Gen.fs` | `hedgehog_gen` | - -Naming a module after the bare basename of its file, as the backend used to, breaks in three ways -— all silent at compile time and fatal at runtime: - -1. **OTP is shadowed.** `Gen.fs`, `Random.fs`, `String.fs`, `Timer.fs`, `Queue.fs`, ... all name - real OTP stdlib modules. OTP's win, and calls into the generated code raise `undef`. -2. **Assemblies overwrite each other.** Two `DSL.fs` files in two projects both emit `dsl.erl` - into the flat output `src/`; the one compiled last overwrites the other **on disk**, and the - loser's functions vanish from the output entirely. -3. **Files within an assembly collide** the same way (`Foo/Types.fs` vs `Bar/Types.fs`). - -Two exemptions: - -- **fable-library** keeps its bare, hand-maintained names (`fable_list`, `seq`, `range`, ...). - It is the one project whose *compiled* output ships as a dependency, and `getLibPath` in - `Transforms.Util` refers to its modules by exactly those names. -- **Native Erlang modules** reached through `BeamInterop` (`string`, `lists`, ...) are of course - referenced by their own names. - -Naming lives in one place, `Fable.Beam.Naming.erlangModuleName` (`Beam/Prelude.fs`), because the -code generator (which must resolve an import to the atom the imported file declared) and the CLI -(which must write the file under the name of the module inside it) have to agree exactly. - -Qualification is a convention, not a guarantee, so `checkBeamModuleNames` (`Fable.Cli/Main.fs`) -**fails the build** on the two ways it can still go wrong, rather than letting either surface as an -`undef` at runtime: - -- two source files mapping to the same module name — it names both files; -- a module name that is one of OTP's own (`Naming.otpModules`). Qualification rules out the bare - names, but a two-segment name can still land on a real OTP module — an app named `Gen` with a - `Server.fs` produces `gen_server` — and fable-library's exempt modules are not qualified at all, - so a `Timer.fs` added to it would silently shadow OTP's `timer`. +- `fable-library-beam` keeps its established module names such as `fable_list` and + `seq`; +- native Erlang modules referenced through interop keep their native names. -### Entry point +Function, field, case, and module names are sanitized to Erlang atoms. Reserved +words receive a trailing underscore. Reflection metadata stores both the original +F# name and the emitted atom when both are needed. -Since module names are qualified, the entry point of a project compiled from `Program.fs` is -`my_app_program:main/0`, not `main:main/0`. Fable therefore also emits a small `src/main.erl` -shim exporting `main/0` and `main/1` that forwards to it, so runners have a stable, well-known -entry module: +The generated project contains: -```sh -erl -noshell -pa src -eval "main:main([])" -s init stop +```text +/ + rebar.config + src/ + .app.src + main.erl + _.erl + fable_modules/ + fable-library-beam/ + rebar.config + src/ + fable_library_beam.app.src + ... ``` -As elsewhere in Fable's Beam output, the entry point is the *last* source file of the project: -its module-level actions compile to that module's `main/0`. - -## Type Mappings - -### Natural fits (F# → Erlang) - -| F# | Erlang | Notes | | -| ---------------- | ---------------------- | ------------------------------------------ | ---------------------------- | -| `int`, `float` | `integer()`, `float()` | Direct | | -| `string` | `binary()` | `<<"hello">>` | | -| `char` | `integer()` | Unicode codepoint; see caveat below | | -| `bool` | `true \ | false` | Atoms | -| `unit` | `ok` | Atom | | -| `tuple` | `tuple` | Direct: `{A, B, C}` | | -| `list` | `list()` | Both are linked lists! | | -| `option` | `x` or `{some, x}` \ | `undefined` | Erased or wrapped (see below)| -| `Result` | `{ok, V} \ | {error, E}` | Matches Erlang idiom exactly | -| Pattern matching | Pattern matching | Both languages excel here | | -| Immutability | Immutability | Erlang is immutable by default | | -| `bigint` | `integer()` | Erlang has native arbitrary-precision ints | | - -### Requires design decisions - -| F# | Erlang Strategy | Alternatives | -| --------------------------------- | ------------------------------------------------- | ------------------------------------ | -| **DU cases** | Tagged tuples: `{some, V}`, `{node, L, R}` | Maps, records | -| **Records** | Erlang maps: `#{name => <<"Dag">>}` | Erlang records (compile-time tuples) | -| **Classes** | Module + map (state as map, methods as functions) | Processes holding state | -| **Interfaces** | Dispatch maps: `#{method => fun(...) -> ... end}` | Done (object expressions) | -| **Mutability** (`ref`, `mutable`) | Process dictionary, ETS, or process state | Agent pattern | -| **Exceptions** | `throw`/`catch` with tagged tuples | Error tuples (Erlang way) | -| **Generics** | Erased (Erlang is dynamically typed) | — | -| **Currying** | Lambda wrapping (same as Python target) | — | -| **Nested modules** | Flat module names: `My_Module_Sub` | One file per module | -| **Computation expressions** | Transformed at Fable AST level; async/task → CPS | — | - -### Known limitation: `char` at a generic type stringifies to its codepoint - -A `char` is a plain `integer()` at runtime — the same representation as an `int`, and the same -choice the Dart target makes. Nothing at runtime can tell the two apart, so converting a `char` to -a string is correct only where the compiler can still see the type statically. It can in every -ordinary case: - -```fsharp -string c // "2" -c.ToString() // "2" -"x" + string c // "x2" -System.Char.ToString c // "2" -$"{c}" // "2" -string (box c) // "2" — the boxing cast is still visible at the call site -System.String.Format("{0}", c) // "2" -System.String.Format("{0}", box c) // "2" -``` +Fable regenerates its own scaffold files. An existing user-owned `rebar.config` +is left unchanged. -Boxing only survives while the cast is part of the same expression. Once the boxed char is bound to -an `obj` — by a `let`, or by flowing through a pipe — the static type is gone before the conversion -is reached, and these degrade to the generic case below: +### Entry point -```fsharp -box c |> string // "50" — the pipe binds it as obj first -let b: obj = box c in string b // "50" -sprintf "%O" c // "50" — no char converter is currently threaded to printf -sprintf "%A" c // "50" — .NET gives "'2'" -[ box '2' ] |> List.map string // ["50"] — element type is obj -``` +The last F# source file owns module-level actions and the program entry point. The +CLI emits `src/main.erl` as a stable shim: -The `%O`/`%A` cases are not a boxing problem but a printf one: `printf` parses its format string and -applies its arguments through a curried runtime function. The compiler now preserves one narrow -piece of static information across that boundary: when a concrete argument type overrides -`System.Object.ToString()`, it supplies a converter for that argument and `%O` invokes it. This fixes -custom record and union formatting without changing their runtime representation. It does not yet -supply converters for primitive representations such as `char`, or the richer type information -needed by `%A`; see "Object overrides and printf" and "Structured formatting (`%A`)" below. - -But when `string x` is applied where `x`'s type is a *generic parameter*, the backend can only emit -`fable_convert:to_string/1`, which sees an integer and prints the number: - -```fsharp -// F# generalizes this to Op<'a> -> Op, so `string c` is applied at a generic type. -let toStr = - function - | Keep c -> Keep(string c) - | Drop c -> Drop(string c) - -[ Keep '2'; Drop '1' ] |> List.map toStr // "5049", not "21" +```sh +erl -noshell -pa _build/default/lib/*/ebin \ + -eval 'main:main([])' -s init stop ``` -The trigger is F# generalizing a let-bound lambda, which is easy to hit by accident — the DU is -incidental. `%A` and structural printing of a boxed char have the same limitation. +`main:main/0` forwards to the generated entry module. `main:main/1` forwards command +line arguments. An integer F# entry-point result becomes the VM exit code. The shim +also sets `standard_io` and `standard_error` to Unicode. -Fable erases generics by design, so there is no type witness to dispatch on. The fix is the same -one the compiler already recommends when `typeof<'T>` fails for this reason (see -`genericTypeInfoError` in `Replacements.Util.fs`): make the function `inline`, so the type is -resolved at the call site. +## Runtime representation -```fsharp -let inline toStr op = - match op with - | Keep c -> Keep(string c) - | Drop c -> Drop(string c) +| F# value | Erlang representation | Notes | +| --- | --- | --- | +| `unit` | `ok` | Non-final unit expressions are removed by the printer | +| `bool` | `true` / `false` | Atoms | +| Signed and unsigned integers | `integer()` | Width restored after operations that can overflow | +| `bigint` | `integer()` | No width wrapping | +| `float` | `float()` | Erlang floating point | +| `decimal` | scaled `integer()` | Value multiplied by 10^28 | +| `string` | UTF-8 `binary()` | Not a charlist | +| `char` | `integer()` | Unicode codepoint; see known limitations | +| Enum | `integer()` | Casts erase to the underlying number | +| `[]` | atom | `[]` literals remain literals | +| Tuple | tuple | `{A, B}` | +| `list<'T>` | list | Native linked list | +| `array<'T>` | process-dictionary reference to a list | Mutable and process-local | +| `byte[]` | `{byte_array, Size, AtomicsRef}` | O(1) reads and writes through `atomics` | +| `seq<'T>` | Fable sequence runtime | Compiled `Seq.fs`/`Seq2.fs` preserve lazy paths | +| `Map<'K,'V>` | map | Native key ordering applies | +| `Set<'T>` | ordset | Sorted native list | +| `option<'T>` | erased value or `undefined` | Ambiguous nested/generic cases use `{some, Value}` | +| `Result<'T,'E>` | `{ok, Value}` / `{error, Error}` | Erlang convention | +| Nullary union case | atom | Sanitized case tag | +| Union case with fields | tuple | `{case_tag, Field1, ...}` | +| Record / anonymous record | map | Sanitized field atoms | +| Immutable class | map | Used when construction does not require self-reference or mutation | +| Mutable/self-referencing class | process-dictionary reference | State is process-local | +| Interface / object expression | map of values and closures | Calls dispatch through map entries | +| Exception | map | Includes `message` and, for custom exceptions, `exn_type` | +| Function | Erlang fun | Curry/uncurry adapters preserve identity where supported | +| Local mutable / ref cell | process-dictionary reference | Erased at scope exit where possible | + +## Implemented features + +The checklists record current capabilities, not development phases. + +### Language and code generation + +- [x] Literals, tuples, lists, maps, functions, delegates, and partial application +- [x] Arithmetic, logical, bitwise, relational, and equality operators +- [x] `if`, loops, sequence expressions, ranges, and pipelines +- [x] Pattern matching, active patterns, decision trees, and guards +- [x] Recursive and mutually recursive functions +- [x] Tail calls and named recursive Erlang funs +- [x] Records, anonymous records, discriminated unions, options, results, and enums +- [x] Class construction and common instance/member calls; interfaces and object expressions +- [x] Custom exceptions, `AggregateException`, `try`/`with`, `try`/`finally`, `raise`, `failwith`, and `failwithf` +- [x] Modules, nested modules, imports, exports, and cross-file calls +- [x] Module-qualified names and OTP module collision detection +- [x] Quotations and derived quotation patterns +- [x] Compile-time and runtime reflection metadata +- [x] `FABLE_COMPILER_BEAM` conditional compilation symbol + +### Numerics, text, and time + +- [x] Fixed-width signed and unsigned integer wrapping +- [x] `int64`, `uint64`, `bigint`, and decimal arithmetic +- [x] Integer and floating-point conversions and parsing +- [x] String, character, encoding, regular-expression, and URI operations +- [x] `sprintf`, `printf`, `printfn`, `eprintfn`, `failwithf`, and `String.Format` +- [x] Static dispatch to custom `ToString()` for direct calls, `string`, and `%O` +- [x] `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`, and `TimeSpan` +- [x] `Guid`, `Random`, `Stopwatch`, `BitConverter`, and environment APIs + +### Collections + +- [x] `List`, `Seq`, `Array`, and `Array.Parallel` +- [x] `Map` and `Set` +- [x] `ResizeArray`, `Dictionary`, `HashSet`, `Queue`, and `Stack` +- [x] Mutable byte arrays backed by `atomics` +- [x] Structural collection equality and comparison on supported element shapes +- [x] Enumeration through lists, references, maps, and mutable collections + +### Effects and concurrency + +- [x] `Async` computation expressions +- [x] `Task` computation expressions mapped to the Async runtime +- [x] `Async.StartChild`, `AwaitTask`, `Parallel`, `Sequential`, and continuations +- [x] Cancellation tokens and cancellation-aware sleep +- [x] `MailboxProcessor` +- [x] Observables and events +- [x] Parallel array operations using spawned worker processes + +### Interop and tooling + +- [x] `[]` inline Erlang expressions +- [x] `[]` native module calls +- [x] `[]` with erased interfaces for typed module bindings +- [x] Erlang keyword and quoted-atom escaping +- [x] rebar3 scaffold generation +- [x] Runtime-library placement under `fable_modules` +- [x] Quicktest support +- [x] .NET, generated Erlang, native Erlang, and entry-point tests + +## Core semantics + +### Replacements + +Call lowering follows this order: -[ Keep '2'; Drop '1' ] |> List.map toStr // "21" +```text +Beam.Replacements.tryCall + -> Beam-native operator or OTP/runtime call + -> JavaScript replacement fallback when target-neutral ``` -Annotating the parameter concretely (`Op`) works too, for the same reason — it defeats -generalization. - -Giving `char` a tagged runtime form such as `{char, 50}` would fix this everywhere, but it is a -breaking change to a core type, costs arithmetic and comparison performance, and fights Erlang's own -convention that strings are lists of integer codepoints. Not worth it for a case with a one-keyword -workaround. - -### Object overrides and printf - -Records and unions are still bare maps, tuples, or atoms, so a runtime value cannot dynamically -look up an attached method. Custom `System.Object.ToString()` overrides are instead resolved while -the concrete F# type is available: - -- `ObjectOverrides` in `Prelude.fs` owns override detection, generated function naming, and - zero-argument call construction. -- Declaration emission keeps `System.Object` overrides as module-level functions instead of putting - them in an instance dispatch map that a record or union value cannot carry. -- Direct `value.ToString()` and `string value` use `ToString.toStringByType`, which first tries the - statically resolved override and then applies the built-in type-specific conversions. -- `PrintfFormat<'Printer, _, _, _>` exposes its argument types as a nested lambda chain. The - `PrintfFormat` replacement uses those types to build a parallel list containing a custom - `ToString` converter or `undefined` for each argument. `fable_string:printf/2` carries the list - through the curried printer and applies a converter only for `%O`; `printf/1` remains the - backward-compatible path when no converters are needed. All consumers of that format object, - including `sprintf`, `printfn`, `eprintfn`, and `failwithf`, share the behavior. - -For example, both expressions below now produce `Untrusted` on Beam, matching .NET: - -```fsharp -type IntegrityLevel = - | Untrusted - | Trusted - - override this.ToString() = - match this with - | Untrusted -> "Untrusted" - | Trusted -> "Trusted" - -IntegrityLevel.Untrusted.ToString() -sprintf "%O" IntegrityLevel.Untrusted -``` +BEAM replacements own numerics, equality and comparison, collections, conversion, +formatting, Async/Task, reflection, mutable storage, and Erlang interop. This avoids +JavaScript-only modules and injected comparers/adders. -The Beam record and union tests cover direct calls, the `string` operator, `%O`, mixed format -arguments, partial application, and a cross-file call. This keeps static override lookup, generated -imports, and the printf transport from regressing independently across the atom/tuple and map -representations. - -This is intentionally static dispatch. If the value has already been erased to `obj`, or its type -is an uninlined generic parameter, the call site no longer identifies the declaring type and the -runtime fallback remains responsible for formatting it. The optional formatter list also currently -tracks the `PrintfFormat` argument sequence directly. Any future generalization must account for -format specifiers such as `%a`, `%t`, and `%*d`, whose consumed arguments do not have a simple -one-specifier/one-value correspondence. - -#### Proposed follow-up: an argument-slot formatting plan - -The optional converter list is a deliberately narrow bridge for custom `%O`; it should not grow -into a collection of unrelated formatter flags. Its entries follow the `PrintfFormat` argument type -sequence, while the runtime currently consumes one entry per parsed format specifier. That happens -to align for ordinary `%O`, but it is not the general printf model: - -- `%O`, `%A`, and `%d` consume one value argument. -- `%*d` consumes a width and a value. -- `%a` consumes a printer and a value. -- `%t` consumes a thunk. - -Before adding type-directed formatting for more cases, replace the simple correspondence with an -explicit argument-slot plan. The compiler should build the plan centrally from the format and its -`PrintfFormat` lambda chain. Each slot describes its role (value, width, printer, or thunk) and may -carry a statically selected value formatter. The runtime then consumes slots according to the -specifier's actual arity instead of assuming one specifier equals one argument. - -The current custom `ToString` closure becomes the first value-formatter hint in that plan. Later -hints can cover representation-ambiguous primitives such as `char`, `decimal`, `DateTime`, and -`TimeSpan`, and richer descriptors or generated recursive formatters can support `%A`. String -interpolation, `System.String.Format`, and `Console` overloads should reuse the same centralized -static formatting selection rather than acquire independent type-dispatch tables. - -Dynamic formats, values already erased to `obj`, and uninlined generic parameters still need the -runtime shape-based fallback. The plan transports information available at a typed call site; it -does not make values self-describing or change their Erlang representation. - -### Structured formatting (`%A`) reads shapes, not types - -`%A` renders a value in F# syntax. The other targets get this for free because their generated types -carry a real `ToString` — a JS record is a class instance, a Python record defines `__str__` — so the -formatter just calls it. Beam has nothing to call: a record is a bare Erlang map and a union a bare -tagged tuple, neither carrying any back-pointer to its type. Reflection does not help either, since -every `fable_reflection` accessor takes a type-info map and a runtime value cannot produce one. - -So `fable_string:format_any/1` dispatches on the *shape* of the term. That gets the common cases -right — strings are quoted, lists print as `[a; b]`, tuples as `(a, b)`, unions as `Case value`, and -arrays are dereferenced from their ref cell to `[|a; b|]` instead of printing an opaque `#Ref<...>`. - -Where several F# types share one Erlang shape, the ambiguity is unresolvable and `%A` deviates from -.NET: - -| Case | .NET | Beam | Why | -| --- | --- | --- | --- | -| record field order | declaration order | Erlang term order | a map does not remember key insertion order | -| record/union names | original casing | reconstructed | names compile to lowercased atoms; `my_case` → `MyCase` is a convention, not a recovery | -| `Some x` | `Some x` | `x` | `option` is erased (JS and Python collapse it too) | -| `set [1; 2]` | `set [1; 2]` | `[1; 2]` | an F# `Set` is an ordset, i.e. a plain list | -| `(Empty, 1)` | `(Empty, 1)` | `Empty 1` | `{empty, 1}` is also the shape of a one-field union case | -| `'x'` | `'x'` | `120` | a `char` is an `integer()`, per the limitation above | -| `ref 5` | `{ contents = 5 }` | `5` | a ref cell is the same process-dictionary reference an array is, with no `contents` field to name | -| `ref [1; 2]` | `{ contents = [1; 2] }` | `[\|1; 2\|]` | the same collision, landing on the array rendering because the stored value is a list | -| `1.0M` | `1.0M` | `10000000000000000000000000000` | a `decimal` is a fixed-scale integer (value × 10²⁸) and is indistinguishable from one | -| `DateTime(...)` | `1/2/2024 3:04:05 AM` | `(638396498450000000, 1)` | a `DateTime` is a `{Ticks, Kind}` tuple; likewise `TimeSpan`, which is a bare integer | - -The last four are worse than the ambiguities above them, in that the value is not merely rendered in -the wrong style but is unreadable. They are still shape collisions rather than bugs — nothing about -the runtime term says "this reference is a ref cell, not an array" or "this integer is scaled" — and -all four print at least as well as the `~p` dump they replaced. - -Recovering these cases needs more than the optional `ToString` converters now threaded for `%O`. -The argument-slot plan described above should carry compact type descriptors or generated recursive -formatters for `%A`; full reflection maps are unnecessary. Generic parameters that erase to a -placeholder still use the shape-based fallback. - -Persisting types in every runtime object is the alternative. A stable type token on records, -unions, and class instances could support dynamic override dispatch, reflection, serializers, -type tests, and unambiguous recursive `%A` formatting even after values flow through `obj`. But this -is not a local formatting fix: nullary union cases could no longer remain bare atoms, records and -union values would need wrappers or reserved metadata fields, and construction, pattern matching, -equality, comparison, hashing, JSON/interop, and package compatibility would all be affected. A -full type-info value is also a poor payload because it may contain functions and other terms with -awkward equality semantics; a compact token plus a registry or generated reflection function would -be safer. - -For now, Beam keeps its compact idiomatic representations and threads only the static information a -specific call site needs. Self-describing values remain a possible future ABI revision rather than -an incremental extension of the current object schema. - -Anything the formatter does not recognise — pids, ports, a cyclic term past the depth cap — falls -back to `~tp`, which is what `%A` did for *everything* before, so it can never be worse than it was. - -### Console output requires a unicode io device - -An F# `string` is a UTF-8 `binary()`, and every path that prints one — `Console.Write`, -`Console.WriteLine`, `printf`/`printfn`, `eprintfn` — writes it with io's `t` (unicode) modifier -(`~ts`). That reaches the terminal intact only on a device whose encoding is `unicode`; on the -latin1 default of `erl -noshell` a codepoint above latin1 comes out as a `\x{2713}` escape instead. - -Programs started through Fable's generated `main.erl` need to do nothing: the shim's `setup_io/0` -sets both `standard_io` and `standard_error` to unicode before calling into F#. Anything else — -a Fable-compiled module called from a hand-written OTP release, an `escript`, a `gen_server` -started by someone else's supervisor — has to set the device itself: +### Lowering invariants + +| Area | Invariant | +| --- | --- | +| Curried calls | Apply one argument at a time. Combining curried arguments into one Erlang call changes arity. | +| Unit arguments | Remove trailing unit parameters and their call-site arguments symmetrically. | +| Expression blocks | Hoist leading matches before calls, operators, and literals; Erlang argument positions cannot contain Fable statement blocks. | +| Recursion | Emit self-recursive lambdas as named funs; emit a mutual-recursion group through one tagged dispatcher. | +| BIF names | Qualify known BIF calls with `erlang:` and emit `no_auto_import` when a generated local function shares an auto-imported name. | +| Arrays at FFI boundaries | Cancel an inline `new_ref`/`get` pair only when every use of that literal argument is dereferenced. Bound arrays retain reference semantics. | +| Mutable locals | Erase generated process-dictionary keys at scope exit where their lifetime is known. | +| Module initialization | Preserve declaration order and isolate each initializer's Erlang variables before merging it into `main/0`. | +| Unit expressions | Remove non-final bare `ok` expressions without removing a final function result. | +| Classes | Choose a self-contained map only when fields and stored closures do not require mutation or `this`; otherwise use a process-local reference. | + +### Fixed-width integers + +Erlang integers are arbitrary precision. `fable_int.erl` restores .NET widths with +bit-syntax wrapping: ```erlang -ok = io:setopts(standard_io, [{encoding, unicode}]), -ok = io:setopts(standard_error, [{encoding, unicode}]). +wrap_i32(N) -> + <> = <>, + V. ``` -The `+pc unicode` VM flag is *not* a substitute: it only affects printable-list detection in `~p`, -not the device encoding. +The compiler wraps operations that can leave the type's range: addition, +subtraction, multiplication, left shift, negation, complement, and narrowing +conversion. Shift counts are masked to the .NET width. Operations that cannot grow +an in-range value remain native. `bigint` and decimal are not width-wrapped. -There is no device-independent alternative to fall back on. `io:put_chars` follows the device -encoding for both binaries and codepoint lists, so writing a string's raw bytes with `~s` would -merely move the corruption to unicode devices instead of latin1 ones. +Unsigned values keep their unsigned range, so `UInt64.MaxValue` is represented as +`18446744073709551615` and right shift is logical. -### Class instance representation: map vs process-dict ref +### Equality, ordering, and hashing -A class instance has two possible representations, chosen per class at construction: +- Native `=:=` supplies deep equality for numbers, atoms, binaries, tuples, lists, + and maps. +- `erlang:phash2/1` supplies general hashing. +- Direct comparison of a statically known union uses + `fable_comparison:compare_union/3` with case declaration order. +- Direct union relational operators, `compare`, `min`/`max`, and plain + `List.sort`/`Array.sort` use the same declaration-order comparison. +- Function physical equality unwraps compiler-generated curry/eta adapters before + comparing the underlying fun. -- **Immutable classes** (no mutable instance fields, no `as self`, no base-class - state to merge, and whose stored interface closures / field initializers don't use - `this` as a value) are emitted as a **self-contained map** — the instance term *is* - the state map, exactly like a non-self-referencing object expression. These are - process-portable: an interface or regular method invoked from another process reads - fields from the value itself, not from the constructing process's dictionary. -- **Mutable classes** (any `let mutable` / `val mutable` instance field, self-reference, - etc.) keep their state in the **process dictionary**, keyed by a `make_ref()`. The BEAM - has no shared mutable memory across processes, so mutation is single-process by design - — cross-process object sharing of mutable instances is intentionally unsupported (use - actors / message passing for that). +The runtime union representation does not carry the declaration index. Generic +comparison paths that lack static union type information therefore fall back to +Erlang term order. See the roadmap. -Field reads are decoupled from the representation: `fable_utils:field_get/2` (and -`iface_get`, `inst_state`, `move_next`, `get_current`, `safe_dispose`) accept both a map -and a ref, so call sites and runtime helpers work regardless of which form the -constructor chose. See `transformClassDeclaration` in `Fable2Beam.fs`. +### Mutability and process locality -## Concurrency & Async +Local mutables, non-byte arrays, mutable collections, and mutable class state use +`make_ref()` keys in the process dictionary. Benefits and constraints for those +process-dictionary-backed values: -This compiler/runtime implements F#'s async and agent model **in-process** (CPS-based), -keeping mutable process-dict state reachable. Real OTP process concurrency (`gen_server`, -supervision, distribution) is layered on top via the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings — see -"OTP Processes, Supervision & Actors" below. +- mutation is isolated to the owning process; +- a value sent to another process does not carry its mutable state; +- non-byte list/map collection updates replace the stored collection; +- mutable local keys are erased when their scope exits where code generation can + prove the lifetime. -### MailboxProcessor — In-Process CPS Model (IMPLEMENTED) +Module-level mutables use module-qualified process-dictionary keys and initialize +through the generated module's `main/0`. Immutable module values that read a mutable +are snapshotted in declaration order. This is correct only in a process that has run +that initializer. -F#'s `MailboxProcessor` uses same-process CPS continuations (matching JS/Python targets), -NOT OTP gen_server. This design was chosen because: +Byte arrays are the exception. A `byte[]` carries an `atomics` reference in its +`{byte_array, Size, AtomicsRef}` value. Sending that value to another process shares +the backing storage, and `byte_array_set/3` mutations are visible in both processes. -1. F# MailboxProcessor body closures capture mutable state from the caller — a separate - Erlang process can't access process dict state from the parent -2. The CPS async framework (`run_synchronously`) uses process dict `put`/`erase`, - requiring same-process execution -3. gen_server would require compile-time extraction of message handlers from an opaque - async body — extremely complex for no semantic benefit +Use OTP processes and message passing for shared application state. Do not pass a +process-dictionary-backed mutable F# object to another process and expect +shared-object semantics; treat a byte array passed between processes as explicitly +shared mutable state. -The implementation mirrors `src/fable-library-py/fable_library/mailbox_processor.py`. - -| F# | Erlang | -| --- | --- | -| `new MailboxProcessor(body)` | `fable_mailbox:default(Body)` — creates agent with empty queue | -| `MailboxProcessor.Start(body)` | `fable_mailbox:start(Body)` — create + start | -| `agent.Start()` | `fable_mailbox:start_instance(Agent)` — run body via `start_immediate` | -| `inbox.Receive()` | `fable_mailbox:receive_msg(Agent)` — returns `Async` via `from_continuations` | -| `agent.Post(msg)` | `fable_mailbox:post(Agent, Msg)` — queue + `process_events` | -| `agent.PostAndAsyncReply(f)` | `fable_mailbox:post_and_async_reply(Agent, F)` — reply channel + `Async` | -| `replyChannel.Reply(v)` | `(maps:get(reply, Channel))(V)` — emitExpr inline | +### Async, Task, and MailboxProcessor -**OTP actors live in Fable.Beam**: Real process-isolated actors (`gen_server` / -`gen_statem`, supervision, fault tolerance) are provided by the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) OTP bindings and the -[Fable.Actor](https://github.com/fable-hub/Fable.Actor) library — not this in-process -MailboxProcessor. +`Async<'T>` is a cold continuation-passing function: -### Async & Task — In-Process CPS (IMPLEMENTED) +```text +Async = fun(Context) -> ok end +Context = #{on_success, on_error, on_cancel, cancel_token} +``` -`Async` compiles to a **continuation-passing-style (CPS) function**, not an Erlang -process: +`RunSynchronously`, `StartImmediate`, `StartWithContinuations`, and sequential +composition execute in the caller's process so process-local mutable state remains +visible. `Async.Parallel`, `Async.StartChild`, and parallel array operations spawn +workers; those workers do not share the caller's process dictionary. -```erlang -Async = fun(Ctx) -> ok end -Ctx = #{on_success, on_error, on_cancel, cancel_token} -``` +`Task` uses the same runtime and is not a separate hot-task abstraction. +`MailboxProcessor` also uses in-process CPS and a process-local queue. Real OTP +processes, `gen_server`, supervisors, ETS, and distribution come from external +bindings. -The function is *cold* — it does nothing until invoked with a context, matching F#'s -cold-async semantics. Composition (`bind`, `return`, `try/with`, `while`, `for`, …) just -threads new contexts through these functions; see `fable_async_builder.erl`. +### Formatting and Unicode -**Everything runs inline in the caller's process.** This is deliberate, not a -limitation: the CPS body reaches mutable state stored in the process dictionary (mutable -`let`, ref cells, arrays, MailboxProcessor queues), and running in the same process keeps -that state reachable. `RunSynchronously`, `StartImmediate`, and `StartWithContinuations` -all execute the chain in the current process — no `spawn`, no trampoline (Erlang has -native TCO). +Strings are UTF-8 binaries. Generated console calls use Unicode-aware Erlang `io` +formats. The generated `main.erl` configures both standard devices; native launchers +must do the same: -| F# | Erlang (`fable_async` / `fable_async_builder`) | -| --- | --- | -| `async { return x }` | `fun(Ctx) -> (maps:get(on_success, Ctx))(X) end` | -| `let! x = comp` / `do!` | `bind(Comp, fun(X) -> ... end)` — CPS monadic bind | -| `return` / `return!` | `return/1` / `return_from/1` | -| `try/with`, `try/finally` | `try_with/2`, `try_finally/2` (CPS `on_error`/compensation + Erlang try/catch) | -| `while` / `for` | `while/2` / `for/2` (recursive bind) | -| `Async.StartImmediate` | `start_immediate/1` — inline, default context (fire-and-forget) | -| `Async.RunSynchronously` | `run_synchronously/1` — inline, result stashed via a process-dict ref | -| `Async.StartWithContinuations` | `start_with_continuations/4` — inline with caller continuations | -| `Async.Sleep` | `timer:sleep(Ms)` inline; with a cancel token, `receive` waits on a timer/cancel message (still same process) | -| `Async.Sequential` | run each computation inline via `run_synchronously` | -| `Async.Catch` | `catch_async/1` — wraps result in `{choice1_of2,_}` / `{choice2_of2,_}` | -| `Async.Ignore` | `bind` + `return ok` | -| `Async.FromContinuations` | `from_continuations/1` — lower-level CPS primitive | -| `task { ... }` | alias for async (Task is an alias for Async on Beam) | -| `task.Result` | `fable_async:run_synchronously(Comp)` | - -**The one exception — `Async.Parallel` spawns.** To get real parallelism it `spawn`s one -process per child computation and collects results in order via message passing -(`fable_async:parallel/1`). Each child runs `run_synchronously` in its own process, so -parallel children do **not** share the parent's process-dict state. This is the only -place the async runtime leaves the caller's process. - -Cancellation (`fable_cancellation.erl`) is also in-process: a token is a `make_ref()` -keying a process-dict map `#{cancelled, listeners, next_id}`. `Async.Sleep` is the only -operation that observes it cooperatively (via the `receive` path above). - -### OTP Processes, Supervision & Actors — Provided by Fable.Beam (separate library) - -Real BEAM concurrency — spawning processes, `gen_server`, `supervisor`, supervision -trees, applications, ETS, distribution — is **not** part of this compiler or runtime -library. It lives in the separate -[Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings library, which provides -typed F# bindings to OTP modules (`Fable.Beam.GenServer`, `Fable.Beam.Supervisor`, -`Fable.Beam.Application`, `Fable.Beam.Erlang`, `Fable.Beam.Ets`, …) plus an actor model -([Fable.Actor](https://github.com/fable-hub/Fable.Actor)). - -The compiler's job is only to emit correct Erlang; OTP behaviours are opt-in F# bindings -layered on top. Speculative OTP API design (attributes, `supervisor { }` CEs, direct -`gen_server` interop, etc.) belongs in the Fable.Beam repo, not here. - -## Implementation Phases - -### Phase 1: Hello World -- COMPLETE - -Get the full pipeline working end-to-end with minimal features. - -- [x] Add `Language.Beam` to the DU -- [x] Minimal Erlang AST (module, function, expression, literal) -- [x] Minimal Fable2Beam (constants, `printfn`, simple functions) -- [x] Minimal ErlangPrinter (output valid `.erl`) -- [x] CLI integration (`--lang beam` / `--lang erlang`) -- [x] Compile and run: `printfn "Hello from BEAM!"` - -**Goal**: `dotnet fable --lang beam` produces a `.erl` file that `erlc` compiles and runs. - -**What works now**: String/int/float/bool literals, tuples, lists, `printfn`, let bindings, -sequential expressions, type casts, curried apply, emit expressions. Unhandled Fable -expressions produce `todo_*` atom placeholders. The `printfn` chain goes through -`printf` → `toConsole` → `io:format`. - -**How to test**: - -```bash -dotnet build src/Fable.Cli -dotnet run --project src/Fable.Cli --no-launch-profile -- \ - --cwd src/quicktest-beam src/quicktest-beam/quicktest.fsproj \ - --lang beam --outDir /tmp/beam-out --noCache +```erlang +ok = io:setopts(standard_io, [{encoding, unicode}]), +ok = io:setopts(standard_error, [{encoding, unicode}]). ``` -**Note**: Phase 1 quicktest command above is for manual exploration. For the full -automated test suite, use `./build.sh test beam` (see Phase 2). - -### Phase 2: Core Language -- COMPLETE - -Core F# language features that map naturally to Erlang. All implemented in -`Fable2Beam.fs` with corresponding AST additions and printer updates. - -- [x] Arithmetic operators (`+`, `-`, `*`, `div`/`/`, `rem`) with int/float distinction -- [x] Comparison operators (`=:=`, `=/=`, `<`, `=<`, `>`, `>=`) -- [x] Bitwise operators (`band`, `bor`, `bxor`, `bsl`, `bsr`, `bnot`) -- [x] Logical operators (`andalso`, `orelse`, `not`) -- [x] Exponentiation via `math:pow/2` -- [x] If/else → `case Guard of true -> Then; false -> Else end` -- [x] Lambda (single arg, curried) → `fun(Arg) -> Body end` -- [x] Delegate (multi-arg, uncurried) → `fun(A, B) -> Body end` -- [x] CurriedApply → `Apply` node for calling fun values -- [x] Test expressions (`UnionCaseTest`, `ListTest`, `OptionTest`) -- [x] Get expressions (`TupleIndex`, `UnionTag`, `UnionField`, `ListHead`, `ListTail`, `OptionValue`, `FieldGet`, `ExprGet`) -- [x] DecisionTree / DecisionTreeSuccess (following JS target pattern) -- [x] NewList fix → `ListCons` (`[H | T]` instead of `[H, T]`) -- [x] NewUnion → atom-tagged tuples `{atom_tag, Field1, Field2, ...}`, bare atoms for fieldless cases -- [x] NewOption → value or `undefined` atom -- [x] Set expressions (ValueSet → variable rebind, FieldSet → `maps:put`) -- [x] LetRec → sequential fun assignments -- [x] AST additions: `ListCons`, `Apply` on `ErlExpr`; `PList` on `ErlPattern` -- [x] Printer: `BinOp` parenthesization, `UnaryOp` word operator spacing - -**What works now**: Most basic F# programs compile to valid Erlang. Operators, -conditionals, functions (named, anonymous, higher-order, partial application), -pattern matching (integers, strings, booleans, DUs, options, lists, tuples), -decision trees, and let/letrec bindings all produce correct Erlang output. - -**Test suite**: `tests/Beam/` with xUnit. Run with `./build.sh test beam` which: - -1. Runs all tests on .NET via `dotnet test` -2. Compiles tests to `.erl` via Fable (library files auto-copied to `fable_modules/fable-library-beam/`) -3. Compiles library `.erl` files in `fable_modules/fable-library-beam/` with `erlc` -4. Compiles test `.erl` files with `erlc -pa fable_modules/fable-library-beam` -5. Runs an Erlang test runner (`erl_test_runner.erl`) with `-pa fable_modules/fable-library-beam` that discovers and executes all `test_`-prefixed functions - -The Erlang test runner discovers and runs every `test_`-prefixed arity-1 function. The -suite currently has **2446 passing tests across 63 test files** — more than the Python -target. Coverage spans the F# core library and language features: - -- **Collections**: Seq, List, Array (incl. byte arrays via `atomics`), Map, Set, - ResizeArray, Dictionary, HashSet, Queue, Stack -- **Primitives & text**: arithmetic (incl. Int64, BigInt, decimal), bitwise/logical/ - comparison, string, char, regex, conversions, encoding -- **Types**: records, unions, tuples, anonymous records, enums, classes/interfaces, - object expressions, units of measure, structural equality/comparison, reflection -- **Date/time**: DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Guid, Uri, - Stopwatch -- **Control flow & effects**: pattern matching, active patterns, loops, exceptions, - type testing, tail calls, async/task, MailboxProcessor, cancellation, observables/events -- **Interop**: `emitErl`, `Import`, `ImportAll` + `Erase` interfaces, module calls -- **Integration**: a Sudoku solver and a raytracer demo - -See `tests/Beam/` for the individual test files. - -### Phase 3: Discriminated Unions & Records -- COMPLETE - -F#'s defining feature on BEAM. DU basics (construction and pattern matching via -DecisionTree) were implemented in Phase 2. This phase adds records and structural equality. - -- [x] DU declaration → tagged tuple constructors (Phase 2) -- [x] DU pattern matching → clause matching on tagged tuples (Phase 2) -- [x] Records → Erlang maps (`#{field => value}`) -- [x] Record update syntax → F# compiler decomposes into `NewRecord` + `FieldGet` (works automatically) -- [x] Anonymous records → Erlang maps (field names provided inline) -- [x] Structural equality for DUs and records → Erlang's native `=:=` (deep comparison) - -**Design decisions**: - -- Records map to Erlang maps (`#{name => <<"Alice">>, age => 30}`) -- Field names converted to snake_case atoms -- `FieldGet` → `maps:get(field, Map)`, `FieldSet` → `maps:put(field, Value, Map)` -- Structural equality uses Erlang's native `=:=` operator (deep comparison for all - types: tuples, maps, lists, atoms, numbers, binaries) — no runtime library needed. - Implemented via `Beam/Replacements.fs` intercepting `GenericEquality`/`op_Equality` - before JS Replacements generates `Util.equals` library calls. - -### Phase 4: Collections -- COMPLETE - -- [x] `list` → Erlang lists (cons cells — natural fit) -- [x] List module functions → `lists:` module calls + `fable_list.erl` library -- [x] `array` → process dict refs wrapping Erlang lists (mutable via `put`/`get`); byte arrays use `atomics` for O(1) read/write -- [x] `Map` → Erlang native `#{}` maps, `maps:` module calls + `fable_map.erl` -- [x] `Set` → Erlang `ordsets` (sorted lists), `ordsets:` module calls + `fable_set.erl` -- [x] `Seq` → eager Erlang lists with `fable_seq.erl` library -- [x] `fable-library-beam` runtime: `fable_list.erl`, `fable_map.erl`, `fable_string.erl`, `fable_option.erl`, `fable_seq.erl` -- [x] Range expressions: `[1..n]` → `lists:seq(1, n)`, `[1..2..n]` → `lists:seq(1, n, 2)` -- [x] Array indexing: `arr.[i]` → `lists:nth(i + 1, arr)` (0-based to 1-based) -- [x] Array comprehensions: `[| for i in 0..n -> expr |]` via Seq desugaring - -**Design decisions**: - -- Sequences use **lazy evaluation** via Fable-compiled `seq.erl`/`seq2.erl` modules - (compiled from `Seq.fs`/`Seq2.fs`). List-backed operations delegate to `fable_list.erl` BIFs. -- Seq operations intercepted in Beam Replacements (not JS fallback) to avoid - injected comparers/adders that Erlang doesn't need. -- Complex operations in `fable_list.erl`/`fable_seq.erl`, simple BIF mappings via `emitExpr`. -- Scalar Seq operations (Fold, Reduce, Find, etc.) routed through compiled `seq.erl` - (Fable-compiled from `Seq.fs`) via `Helper.LibCall` with `SignatureArgTypes`. This enables - the `uncurrySendingArgs` FableTransform to automatically convert curried callbacks to - uncurried Delegates, matching the pattern used by the Python target. -- BIF qualification: `ErlangPrinter.fs` automatically prefixes known BIFs (`length`, `hd`, - `tl`, `element`, `put`, `get`, etc.) with `erlang:` in `Call(None, ...)` nodes. This - prevents shadowing when compiled library modules (like `seq.erl`) define functions with - the same name as BIFs. -- Integration tested with a Sudoku solver (SudokuTests.fs) using Seq, Array, ranges, and - array comprehensions. -- Sets use Erlang's `ordsets` module (sorted lists). Maintains ordering compatible with - F#'s structural comparison. Simple operations (`add`, `contains`, `union`, etc.) map - directly to `ordsets:*` BIFs. Higher-order operations (`fold`, `map`, `filter`) use - `fable_set.erl` for curried function handling. Set `+`/`-` operators intercepted in - Beam `operators` via `Builtin(FSharpSet _)` arg type matching. `set [1;2;3]` handled - via `CreateSet` → `ordsets:from_list`. - -### Phase 5: Modules & Imports -- COMPLETE - -- [x] F# modules → Erlang modules (one `.erl` per file) -- [x] Nested modules → flattened into the enclosing file's Erlang module via qualified - member names (incl. private, deeply-nested, and shadowed modules — see `MiscTests.fs`) -- [x] Module function calls → `module:function(args)` syntax -- [x] Import resolution and path handling -- [x] Export lists (`-export([...])`) -- [x] Snake_case output filenames (matching Erlang module name convention) -- [x] Module names qualified by their OTP app, so they can neither shadow an OTP module nor - collide across assemblies (see [Module Naming](#module-naming)) -- [x] Function name sanitization (`$XXXX` hex sequences from F# backtick names) -- [x] Cross-module call resolution (derive module from `importInfo.Path`) -- [x] `Assert.AreEqual`/`NotEqual` lowered to `fable_utils:assert_equal`/`assert_not_equal` -- [x] `fable_modules/fable-library-beam/` output structure (aligned with JS/Dart/Rust targets) - -### Phase 6: Error Handling -- COMPLETE - -- [x] try/with → try/catch with `erlang:error` for exceptions -- [x] `failwith` → `erlang:error(<<"message">>)` -- [x] Exception message access via `#{message => Reason}` map wrapping -- [x] Nested try/catch works -- [x] `Result` integration with Erlang `{ok,V}/{error,E}` convention -- [x] Custom F# exception types (`exception MyError of string`) → maps with `exn_type` tag -- [x] Exception type discrimination in catch: `maps:get(exn_type, X, undefined) =:= type_name` -- [x] Multi-field exceptions: `exception MyError2 of code: int * message: string` -- [x] Exception `.Message` property via `message` field in exception map - -### Phase 6b: Types & Type Testing -- COMPLETE - -Extend type system support for common F# patterns. - -- [x] **Enum support** — F# enums are just integers in Erlang (trivial, works out of box) - - Enum declaration, construction, pattern matching — all native - - `int` ↔ enum conversion, `enum(n)` — TypeCast is erased - - Enum comparison, flags (bitwise) — native Erlang operators - - `EnumOfValue`/`EnumToValue` — TypeCast is erased -- [x] **Custom exceptions** — `exception MyError of string` → maps with `exn_type` atom tag - - Exception construction via `NewRecord` adds `exn_type` and `message` fields - - Pattern matching in try/catch: `maps:get(exn_type, X, undefined) =:= type_name` - - Exception `.Message` property via `message` field in exception map - - TryCatch handler preserves exception maps (is_map check), wraps non-maps in `#{message => ...}` - - Multi-field exceptions with named fields work correctly -- [x] **Type testing (`:?`)** — runtime type checks via Erlang guards - - `match x with :? int as i -> ...` → `is_integer(X)` guard - - Primitive types: `is_binary` (string), `is_boolean` (bool), `is_float` (float), `is_integer` (int) - - Collection types: `is_list` (list/array), `is_tuple` (tuple), `is_map` (record/class) - - Exception types: `is_map(X) andalso maps:get(exn_type, X, undefined) =:= type_name` - - `box`/`unbox` are erased (TypeCast) -- [x] **String interpolation fix** — `fable_string:to_string/1` for generic value formatting - - Replaces `~p` format (which showed `<<"...">>` for binaries) with runtime type dispatch - - Handles binary/integer/float/atom natively, falls back to `~p` for complex terms -- [x] **Curry expressions** — uses `Replacements.Api.curryExprAtRuntime` to generate nested lambdas at compile time (no runtime module needed) - -### Phase 7: Async & Task -- COMPLETE - -CPS (Continuation-Passing Style) implementation. `Async` = `fun(Ctx) -> ok end` where -`Ctx = #{on_success, on_error, on_cancel, cancel_token}`. CPS naturally gives cold semantics -(F# Async is cold — doesn't execute until started). Task CE is an alias for Async on the Beam -target since Erlang has no equivalent of .NET's hot Task distinction. - -- [x] `async { }` computation expression → CPS builder via `fable_async_builder.erl` -- [x] `let!` / `do!` → `bind/2` (monadic bind — run computation, pass result to binder) -- [x] `return` / `return!` → `return/1` / `return_from/1` -- [x] `try/with` in async → `try_with/2` (CPS on_error override + synchronous try/catch) -- [x] `while` / `for` in async → `while/2` / `for/2` (recursive bind) -- [x] `Async.RunSynchronously` → runs in same process (preserves process dict access) -- [x] `Async.StartImmediate` → runs with default context (fire-and-forget) -- [x] `Async.Parallel` → spawn one process per computation, collect via message passing -- [x] `Async.Sleep` → `timer:sleep/1` -- [x] `Async.Ignore` → bind + return unit -- [x] `Async.StartWithContinuations` → direct CPS invocation -- [x] `Async.FromContinuations` → lower-level CPS primitive -- [x] `task { }` computation expression → alias for async builder -- [x] `task.Result` → `fable_async:run_synchronously` -- [x] Cancellation tokens → process dict pattern with `fable_cancellation.erl` - -**Design decisions**: - -- **CPS over spawn**: `Async` is a function `fun(Ctx) -> ok end`, not a spawned process. - CPS naturally gives cold semantics matching F# Async. No trampoline needed — Erlang has - native tail call optimization. -- **Everything runs inline in the caller's process** — `RunSynchronously`, `StartImmediate`, - `StartWithContinuations`, and `Sequential` never `spawn`. The only exception is - `Async.Parallel`, which spawns one process per child for real parallelism (children run - `run_synchronously` in their own process and don't share the parent's process-dict state). -- **RunSynchronously runs in same process**: Uses a process-dict ref to store the result, NOT - `spawn` + `receive`. This preserves mutable variable (process dict) access from the async body. -- **Task = Async alias**: Task CE builder methods route to `fable_async_builder`, Task - instance methods (`.Result`, `.GetAwaiter().GetResult()`) route to `run_synchronously`. -- **try_with dual handler**: Both the CPS `on_error` override AND the `try/catch` must invoke - the Handler — synchronous throws (like `erlang:error`) bypass the CPS on_error path. -- **Erlang function naming**: `return`, `for`, `while` are NOT reserved words in Erlang — - they work as function names in remote calls (`fable_async_builder:return(V)`). - -### Phase 8: MailboxProcessor -- COMPLETE - -In-process CPS continuation model (same pattern as JS/Python targets). Uses process dict -for mutable state, `fable_async:from_continuations` for the receive/reply coordination. - -- [x] `MailboxProcessor.Start` → `fable_mailbox:start(Body)` (create + start_immediate) -- [x] `new MailboxProcessor(body)` → `fable_mailbox:default(Body)` (create only) -- [x] `agent.Start()` → `fable_mailbox:start_instance(Agent)` -- [x] `inbox.Receive()` → `fable_mailbox:receive_msg(Agent)` (Async via from_continuations) -- [x] `agent.Post(msg)` → `fable_mailbox:post(Agent, Msg)` (queue + process_events) -- [x] `agent.PostAndAsyncReply(f)` → `fable_mailbox:post_and_async_reply(Agent, F)` -- [x] `replyChannel.Reply(v)` → `(maps:get(reply, Channel))(V)` (emitExpr inline) - -**Design decisions**: - -- **Same-process, not gen_server**: MailboxProcessor body closures capture mutable state via - process dict. A separate process can't access this state. The CPS model runs everything - inline in the caller's process, matching F# semantics exactly. -- **State as process dict map**: Agent = `#{ref => Ref}` where `Ref` keys into process dict - storing `#{body, messages, continuation}`. Mutable queue + continuation slot. -- **Synchronous reply coordination**: `post_and_async_reply` stores a reply callback in the - reply channel map. Since everything runs synchronously via CPS, by the time `post` returns - the inbox has processed the message and called Reply, so the value is available immediately. -- **Named `receive_msg`**: Erlang's `receive` is a reserved keyword, so the function is named - `receive_msg`. The Replacements dispatch maps `"Receive"` → `receive_msg`. -- **OTP actors live in Fable.Beam**: Process-isolated actors and supervision are provided by - the separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) OTP bindings and - [Fable.Actor](https://github.com/fable-hub/Fable.Actor) — not this in-process MailboxProcessor. - -### Phase 9: OTP Patterns — Moved to Fable.Beam - -OTP integration (supervision trees, application behaviour, hot code reloading, -distribution / multi-node) is **out of scope for the compiler**. It is provided by the -separate [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) bindings library -(`gen_server`, `supervisor`, `application`, `ets`, `erlang` process BIFs, …) and the -[Fable.Actor](https://github.com/fable-hub/Fable.Actor) actor model. The compiler only -needs to emit correct Erlang that those bindings can call. - -### Phase 10: Ecosystem - -- [x] Build integration: `rebar3` project generation — `Main.fs` lays out files under - `src/` (rebar3 convention), generates the root `rebar.config`, and per-dependency - `src/.app.src` + `rebar.config`. Fable-generated configs are regenerated; user-owned - ones are detected and left untouched. -- [x] Test suite (`tests/Beam/` — 2446 tests passing, `./build.sh test beam`) -- [x] Erlang test runner (`tests/Beam/erl_test_runner.erl` — discovers and runs all `test_`-prefixed arity-1 functions) -- [x] `erlc` compilation step in build pipeline (per-file with graceful failure) -- [x] Quicktest setup (`src/quicktest-beam/`, `Fable.Build/Quicktest/Beam.fs`) -- [ ] Documentation - -## Erlang AST - -`Beam.AST.fs` is deliberately small (~80 lines) and that proved sufficient for the entire -test suite. Operators are plain strings (not typed DUs), `if` lowers to `case`, and there -is no dedicated `ListComprehension` / `BinaryExpr` / `MapUpdate` node — those F# -constructs are expressed with the existing nodes plus `Emit` as a raw-Erlang escape hatch. -The actual node set: `ErlLiteral`; `ErlPattern` (`PVar`/`PLiteral`/`PTuple`/`PList`/ -`PWildcard`); `ErlExpr` (literals, variables, tuples, lists/`ListCons`, maps, `Call`/ -`Apply`, `Fun`/`NamedFun`, `Case`, `Match`, `Block`, `BinOp`/`UnaryOp`, `TryCatch`, -`Emit`, `Receive`); attributes; function defs; and modules. - -The richer AST below was the *original* aspirational design. It was **never needed** and -is kept only as a reference for what a fuller Erlang AST could look like if a future -feature (e.g. native list comprehensions or bit-syntax literals) ever warrants it: - -```fsharp -module rec Fable.AST.Beam - -type Atom = Atom of string - -type Literal = - | Integer of int64 - | Float of float - | StringLit of string // binary literal <<"...">> - | AtomLit of Atom - | BoolLit of bool - | NilLit // empty list [] - -type Pattern = - | PVar of string - | PLiteral of Literal - | PTuple of Pattern list - | PList of Pattern list * Pattern option // [H|T] pattern - | PCons of Pattern * Pattern - | PWildcard // _ - | PMap of (Pattern * Pattern) list - -type Guard = Expression list // guard sequences - -type Expression = - | Literal of Literal - | Variable of string - | Tuple of Expression list - | List of Expression list * Expression option // [H|T] - | Map of (Expression * Expression) list - | MapUpdate of Expression * (Expression * Expression) list - | BinOp of BinaryOp * Expression * Expression - | UnaryOp of UnaryOp * Expression - | Call of module_: Expression option * func: Expression * args: Expression list - | Fun of FunClause list // fun(Args) -> Body end - | Case of Expression * CaseClause list - | If of IfClause list - | Receive of CaseClause list * Timeout option - | Try of body: Expression list * catch_: CatchClause list * after_: Expression list - | Block of Expression list // begin ... end - | Match of Pattern * Expression // Pattern = Expr - | ListComprehension of Expression * Qualifier list - | BinaryExpr of BinaryElement list // <<"hello">> - -and CaseClause = { Pattern: Pattern; Guard: Guard; Body: Expression list } -and IfClause = { Guard: Guard; Body: Expression list } -and CatchClause = { Class: Atom option; Pattern: Pattern; Guard: Guard; Body: Expression list } -and FunClause = { Patterns: Pattern list; Guard: Guard; Body: Expression list } -and Timeout = { Duration: Expression; Body: Expression list } - -type BinaryOp = Add | Sub | Mul | Div | IntDiv | Rem | Band | Bor | Bxor | Bsl | Bsr | And | Or | Andalso | Orelse | Append | Subtract -type UnaryOp = Not | Bnot | UAdd | USub -type ComparisonOp = Eq | NotEq | Lt | LtE | Gt | GtE | ExactEq | ExactNotEq - -type Qualifier = - | Generator of Pattern * Expression // X <- List - | BinaryGenerator of Pattern * Expression // <> <= Binary - | Filter of Expression - -type BinaryElement = { Value: Expression; Size: Expression option; TypeSpecifiers: Atom list } - -type Attribute = - | Module of Atom - | Export of (Atom * int) list // function/arity pairs - | Import of Atom * (Atom * int) list - | Behaviour of Atom - | TypeSpec of name: Atom * spec: string // -spec - | CustomAttr of Atom * Expression list - -type FunctionDef = - { Name: Atom - Arity: int - Clauses: FunClause list } - -type Form = - | Attribute of Attribute - | Function of FunctionDef - | Comment of string - -type Module = - { Name: Atom - Forms: Form list } -``` +`%O` receives statically selected converters for values with a custom +`System.Object.ToString()` override. `%A` uses runtime term shape because records, +unions, primitives, and mutable references do not carry complete type metadata. -## Sized & Signed Integer Semantics +### Reflection -> **Status: implemented** using Strategy A (bit-syntax wrapping), described below. -> The wrapping helpers live in `src/fable-library-beam/fable_int.erl` (`wrap_i8`..`wrap_i64`, -> `wrap_u8`..`wrap_u64`). Codegen routes the operations that can leave a type's width — -> `+`, `-`, `*`, `bsl`, negation, `bnot` and narrowing conversions — through them, and masks -> shift counts to the width the way .NET does. `band`/`bor`/`bxor`/`bsr`/`rem` cannot grow an -> in-range value, so they stay bare. `bigint` and the fixed-scale `decimal` are never wrapped. -> -> Unsigned types are represented as their unsigned value (0..2^n-1), not as a signed bit -> pattern, so `UInt64.MaxValue` is `18446744073709551615` and `bsr` is a logical shift. +Generated reflection functions return maps containing full names, generic +arguments, record fields, or union cases. Recursive record/union metadata is lazy. +Field and case entries retain both source names and emitted Erlang atoms. -### The Problem +`fable_reflection.erl` implements record, union, tuple, function, and type queries, +plus construction and field/case access. Erased runtime values still limit dynamic +type tests and formatting where two F# types have the same Erlang shape. -.NET has fixed-width integers (`int8`, `int16`, `int32`, `int64`, `uint8`...`uint64`) -with specific overflow/wrapping behavior. Erlang, like Python, has arbitrary-precision -integers — no overflow, no fixed bit width. +### Interop -For Fable.Python this required reimplementing all sized integer types in **Rust via -PyO3** (`src/fable-library-py/src/ints.rs`, ~1200 lines) using `wrapping_add`, -`wrapping_sub`, `wrapping_mul`, etc. Plus typed arrays in Rust for the same reason. -This was a major effort. +| F# | Erlang | +| --- | --- | +| `[]` | `lists:map(...)` | +| `[]` | Inline Erlang expression | +| `[]` erased interface | Typed remote calls such as `gen_server:call(...)` | +| `[]` case | Erlang atom | -### Erlang Advantage: Bit Syntax +Interop values use their actual Erlang representation. Public APIs that expose +records, unions, options, arrays, or mutable objects therefore depend on the target +ABI described above. -Erlang has a feature Python lacks — **binary pattern matching with bit-level -type specifications**. This can express wrapping semantics in pure Erlang: +## Runtime coverage -```erlang -%% Wrapping int32 arithmetic — pure Erlang, no NIF needed --module(fable_int32). --export([add/2, sub/2, mul/2, from_int/1]). +| Area | Runtime modules | +| --- | --- | +| Core values | `fable_option`, `fable_result`, `fable_convert`, `fable_comparison`, `fable_utils` | +| Collections | `fable_list`, `fable_seq`, `fable_map`, `fable_set`, `fable_resize_array`, `fable_dictionary`, `fable_hashset`, `fable_queue`, `fable_stack` | +| Numerics | `fable_int`, `fable_decimal`, `fable_bit_converter`, `fable_random` | +| Text | `fable_string`, `fable_char`, `fable_regex` | +| Date and time | `fable_date`, `fable_date_offset`, `fable_date_only`, `fable_time_only`, `fable_timespan`, `fable_stopwatch` | +| Effects | `fable_async_builder`, `fable_async`, `fable_cancellation`, `fable_mailbox`, `fable_parallel` | +| Events | `fable_observable`, `fable_event` | +| Metadata | `fable_reflection`, `fable_quotation` | +| Platform values | `fable_guid`, `fable_uri`, `fable_environment` | -add(A, B) -> wrap32(A + B). -sub(A, B) -> wrap32(A - B). -mul(A, B) -> wrap32(A * B). -from_int(N) -> wrap32(N). +## Validation -wrap32(N) -> - <> = <>, - V. +Fast iteration: -%% Same pattern for all widths: -%% wrap8(N) -> <> = <>, V. -%% wrap16(N) -> <> = <>, V. -%% wrap64(N) -> <> = <>, V. -%% uwrap32(N)-> <> = <>, V. +```sh +./build.sh quicktest beam ``` -How it works: - -1. `<>` — constructs a 32-bit binary, truncating to low 32 bits -2. `<> =` — pattern-matches it back, interpreting as signed -3. Result: correct two's complement wrapping, same as .NET - -The BEAM JIT compiler (OTP 24+) optimizes binary operations heavily — this is one -of Erlang's most performance-critical paths (telecom/protocol workloads). - -### Strategy Options - -| Strategy | Effort | Performance | Correctness | -| -------- | ------ | ----------- | ----------- | -| **A. Bit syntax wrapping** (pure Erlang) | Low | Good (JIT-optimized) | Exact | -| **B. Band + sign extension** (pure Erlang) | Low | Good | Exact | -| **C. Rust NIF** (like Python) | High | Best | Exact | -| **D. Wrap at boundaries only** | Low | Best | Risky | +Build the runtime library: -**Recommendation: Strategy A (bit syntax)** for initial implementation. - -- No NIF compilation needed — pure Erlang, trivial to deploy -- Correct by construction (bit truncation + signed reinterpretation) -- If profiling shows it's a bottleneck, can move to NIF later -- Much simpler than the Python/PyO3 approach (~50 lines vs ~1200 lines) - -Strategy B alternative using `band`: - -```erlang -%% int32 via bitwise masking -wrap32(N) -> - R = N band 16#FFFFFFFF, - case R >= 16#80000000 of - true -> R - 16#100000000; - false -> R - end. +```sh +./build.sh fable-library --beam ``` -Both A and B are pure Erlang. A is more idiomatic and arguably clearer. +Run the complete target validation: -### Wrapping in Generated Code +```sh +./build.sh test beam +``` -Two approaches for where to wrap: +The full command: -**Wrap every operation** (safe, slower): +1. runs the F# tests on .NET; +2. builds `fable-library-beam`; +3. transpiles `tests/Beam/` to `temp/tests/Beam/`; +4. compiles the generated project with rebar3; +5. runs every exported `test_*` function through `erl_test_runner`; +6. compiles and runs entry-point fixtures through the generated `main.erl` shim. -```erlang -%% F#: let x = a + b * c -X = fable_int32:add(A, fable_int32:mul(B, C)) -``` +The passing-test count is intentionally omitted because it changes whenever the +suite grows. -**Wrap at assignment only** (faster, still correct for most code): +## Known limitations -```erlang -%% F#: let x = a + b * c -X = fable_int32:wrap(A + B * C) -``` - -The second is valid when intermediate overflow doesn't cross the 64-bit boundary -(extremely rare in practice). Start with wrap-every-op for correctness, optimize -later with a compiler flag if needed. +| Area | Current behavior | +| --- | --- | +| Union ordering | Generic comparison, union-valued `sortBy` keys, collection `min`/`max`, nested union fields, and union keys/elements in `Map`/`Set` can use Erlang atom order instead of declaration order. | +| Options | Erasure can still conflate `None`, `Some null`/`Some undefined`, and some nested option paths after static type information is lost. | +| `char` | A generic or `obj`-erased character is an integer at runtime, so `string` and `%A` can print its codepoint. UTF-16 surrogate behavior is not complete. | +| Structured formatting | `%A` reconstructs values from term shape. Record field order, original names, erased options, sets, chars, refs/arrays, decimals, and date/time values can differ from .NET output. | +| Identifiers | Record fields containing spaces or symbols are not fully supported. Sanitized names also need collision diagnostics where distinct F# names produce the same Erlang atom. | +| Type tests | Erlang cannot distinguish integer widths or unrelated F# types with the same runtime shape. Some interface/class downcasts and abstract/base dispatch paths are unsupported. | +| Classes and structs | Mutable record fields, class reference equality, some self-referencing/base constructors, mutually recursive class hierarchies, and default struct construction remain incomplete. | +| Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | +| Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | +| Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | +| Exceptions | Filtered handlers do not yet rethrow every unmatched exception correctly. | +| Numeric APIs | Some byref `TryParse`/`DivRem` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, integer `Log2`, and special floating-point values need parity work. | +| Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | +| Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | +| Hashing | Array hashing currently follows content rather than .NET reference identity. | +| Recursive values | Recursive value bindings that lower through `Lazy` and some inline module-value side effects are incomplete. | +| Diagnostics | Unhandled Fable value kinds and unsupported assignment shapes can still compile to runtime `erlang:error({unsupported_*})` paths instead of failing compilation. | + +For `char` conversion in generic code, making the function `inline` or using a +concrete `char` annotation keeps the type available at the call site. Other entries +above have no general source-level workaround and should remain visible in tests. + +## Related projects + +- [Fable.Beam](https://github.com/fable-compiler/Fable.Beam) — typed Erlang/OTP bindings +- [Fable.Actor](https://github.com/fable-hub/Fable.Actor) — actor model on the bindings +- [Gleam](https://github.com/gleam-lang/gleam) — typed language compiling to Erlang +- [Caramel](https://github.com/AbstractMachinesLab/caramel) — OCaml to Erlang compiler +- [LFE](https://github.com/lfe/lfe) — Lisp on the BEAM + +## Roadmap + +The CLI reports the target as alpha. The next steps are ordered by semantic risk, +not by the age of the feature. Beta means that the documented supported surface is +reliable enough for broader use; it does not require complete .NET parity. This is +the same maturity model used by the established targets: target-specific behavior, +unsupported APIs, and disabled parity cases can remain when they are deliberate and +visible rather than silent correctness failures. + +### Correctness priorities for beta + +| Priority | Gap | Suggested direction | +| --- | --- | --- | +| P0 | Unsupported AST paths can survive compilation | Do not silently emit runtime placeholders for constructs inside the supported surface. Prefer compiler errors with source ranges for statically detectable unsupported constructs. | +| P0 | Union declaration ordering is incomplete | Thread union-aware comparers through `sortBy`, `min`/`max`, nested comparison, and ordered collections. If type-directed routing cannot cover generic containers, define a versioned DU/collection representation change. | +| P0 | Option erasure loses states in generic and null-like paths | Carry the nested-option decision through replacements and collection helpers, or adopt an unambiguous tagged form where erasure is unsafe. | +| P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | +| P0 | Object-model gaps affect valid F# | Fix silent wrong-code paths in the claimed object-model surface. Keep unsupported class and struct forms as explicit exclusions until implemented. | +| P0 | Numeric and byref APIs have correctness gaps | Fix incorrect results in claimed numeric APIs. Missing APIs can remain documented exclusions; restore regression tests as implementations land. | +| P0 | Exception filters can swallow unmatched errors | Preserve Erlang class, reason, and stacktrace and re-raise unchanged when no F# handler matches. | -### Parsing +### Fidelity and diagnostics -.NET integer parsing (`Int32.Parse`, `Int32.TryParse`) with `NumberStyles` support -needs implementation. In Erlang: +| Priority | Gap | Suggested direction | +| --- | --- | --- | +| P1 | Disabled or commented parity cases are not an auditable support boundary | Remove stale skips whose underlying defect is fixed. Track relevant remaining cases as tests, documented exclusions, or linked issues without requiring complete parity for beta. | +| P1 | `%O`, `%A`, interpolation, and `String.Format` have separate type-information needs | Build one compiler-generated argument-slot plan that records value, width, printer, and thunk arguments plus optional static formatters. Reuse it across all formatting entry points. | +| P1 | Runtime shapes cannot distinguish several F# types | Pass compact type descriptors or generated recursive formatters at typed call sites. Treat self-describing record/union values as a versioned ABI option, not an incidental formatting patch. | +| P1 | Type tests and downcasts are shape-based | Add compact type tokens only where F# semantics require nominal identity; keep ordinary data representations untagged where possible. | +| P1 | Warnings do not define the supported surface | Document intentional deviations, make unsupported features actionable diagnostics, and test diagnostic text and source ranges. | +| P1 | OTP/version support is a single minimum statement | Run CI against OTP 25 and the current supported OTP release; publish the tested range. | -```erlang -parse_int32(Str) -> - N = binary_to_integer(Str), - case N >= -2147483648 andalso N =< 2147483647 of - true -> {ok, N}; - false -> {error, overflow} - end. - -parse_int32(Str, 16) -> - N = binary_to_integer(Str, 16), - wrap32(N). -``` +### Performance and operability -### What About Arrays? - -For Python, arrays also required Rust/PyO3 typed storage (`Int32Array`, -`Float64Array`, etc.) because Python lists box every element. - -Erlang is better here: - -- **Erlang tuples** — fixed-size, O(1) element access via `element(Index, Tuple)`, - but immutable (copy-on-update via `setelement/3`) -- **Erlang `array` module** — functional sparse arrays, O(log n) access, good for - large mutable-style arrays -- **ETS tables** — true mutable storage, O(1) access, but heavier setup -- **`atomics` module** (OTP 21+) — mutable integer arrays in shared memory, - excellent for `int32[]` / `int64[]` use cases - -Suggested approach: - -- Small/read-heavy arrays → **tuples** (fast reads, copies on write) -- General case → **`array` module** (functional updates, good enough perf) -- Hot-path mutable int arrays → **`atomics`** (true O(1) mutable access) -- No Rust NIF needed — Erlang's built-in options cover the use cases - -### Summary: Much Simpler Than Python - -| Concern | Python Solution | Erlang Solution | -| ------- | --------------- | --------------- | -| Sized integers | Rust/PyO3 (~1200 lines) | Bit syntax (~50 lines pure Erlang) | -| Integer parsing | Rust/PyO3 (~100 lines) | `binary_to_integer` + bounds check | -| Typed arrays | Rust/PyO3 typed storage | Tuples / `array` module / `atomics` | -| Deployment | Needs Rust toolchain + compilation | Pure Erlang, no external deps | - -This is a significant advantage of targeting BEAM over Python. The bit syntax -alone eliminates the single hardest piece of the Fable.Python runtime. - -## Decisions Made - -- **Strings**: Erlang binaries (`<<"hello">>`) — modern convention, more efficient -- **Module naming**: Snake_case derived from filename (`MyModule.fs` → `my_module`) -- **Replacements strategy**: Beam has its own dispatch in `Replacements.Api.fs` with - JS fallback (`Beam.Replacements.tryCall` → `JS.Replacements.tryCall` if `None`). - Beam handles equality, comparison, numerics, collections, and conversions natively; - only operations that genuinely work the same way fall through to JS. All 15 - `Replacements.Api.fs` functions now have explicit Beam dispatch — `error` returns - the message directly (wrapped by `makeThrow`), `defaultof` returns type-appropriate - zero values, and ref cell operations use the process dictionary (`make_ref` + - `put`/`get`). -- **File structure**: Single `Fable2Beam.fs` for Phase 1 (PHP pattern), split later - as complexity grows (Python pattern) -- **DU representation**: Atom-tagged tuples `{atom_tag, Field1, ...}` for cases with - fields, bare atoms for fieldless cases. Tag names derived via `sanitizeErlangName` - (snake_case). `UnionCaseTest` guards non-fieldless checks with `is_tuple` to handle - mixed DUs. `UnionTag` uses `case is_atom(X)` to dispatch between bare atoms and tuples. -- **If/else**: Uses `case Guard of true -> ...; false -> ... end` rather than - Erlang's limited `if` (which only supports guard expressions) -- **Division**: `div` for integer types, `/` for float — determined by Fable's - type information at compile time -- **Comparison operators**: Erlang's exact equality (`=:=`, `=/=`) rather than - structural (`==`, `/=`), matching F#'s value equality semantics -- **DecisionTree**: Follows the JS (Babel) target pattern — inline targets with - Let bindings, producing nested case expressions -- **Test framework**: xUnit with `[]` attributes (matching Python/Rust targets), - conditional compilation via `Util.fs` for future BEAM-side test execution -- **Records**: Erlang maps (`#{field => value}`), field names as snake_case atoms. - Field access via `maps:get/2`, field update via `maps:put/3`. -- **Structural equality**: Erlang's native `=:=` for all types (no runtime library). - Intercepted in `Beam/Replacements.fs` before JS Replacements generates `Util.equals` - library calls. Works because `=:=` does deep comparison on tuples, maps, and lists. -- **Function name sanitization**: `sanitizeErlangName` in `Prelude.fs` decodes - `$XXXX` hex sequences from F# compiled names (e.g. `$0020` → space), strips - apostrophes, converts to snake_case, collapses/trims underscores, and escapes - Erlang reserved words (e.g. `maybe` → `maybe_`, `receive` → `receive_`). The - keyword escaping uses `checkErlKeywords` against the full OTP 25+ keyword set. - Example: `test$0020infix$0020add$0020can$0020be$0020generated` → `test_infix_add_can_be_generated` -- **Output filenames**: Snake_case, following the Python target pattern. Uses - `Naming.applyCaseRule Core.CaseRules.SnakeCase` in `Pipeline.fs` Beam module. - Erlang requires module name to match filename, so `ArithmeticTests.fs` → - `arithmetic_tests.erl` with `-module(arithmetic_tests).` -- **Library output layout**: Aligned with JS/Dart/Rust targets — library files go to - `fable_modules/fable-library-beam/` under the output directory, not mixed with compiled - project files. `ProjectCracker.fs` uses non-empty `buildDir` to trigger the standard - `copyDir` mechanism. `getOutPath` in `Main.fs` checks `Naming.isInFableModules` to preserve - the subdirectory structure for library files while keeping project files flat in `outDir`. - Erlang resolves modules via code path (`-pa fable_modules/fable-library-beam`) rather than - hierarchical imports. Third-party project output structure: - `output/my_module.erl` + `output/fable_modules/fable-library-beam/{fable_list,fable_string,seq,...}.erl`. -- **Assertions**: `Fable.Core.Testing.Assert.AreEqual`/`NotEqual` lower to - `fable_utils:assert_equal`/`assert_not_equal`, which raise - `#{message, actual, expected}` on failure, matching JS and Python. They used to be - inlined at any call site whose import selector happened to be named `assertEqual`/ - `Testing_equal` (and the `NotEqual` variants), which rewrote the body of *any* - same-named user function; that special case is gone. -- **Unit parameters**: Erlang unused variable warnings suppressed by prefixing unit - parameters with `_` via `toErlangVar` in `Fable2Beam.fs`. -- **discardUnitArg / dropUnitCallArg**: Symmetric unit stripping matching JS/Python/Dart. - `discardUnitArg` strips trailing unit parameters from function definitions (Lambda, - Delegate, ObjectExpr members, MemberDeclaration, class methods/constructors). - `dropUnitCallArg` strips the corresponding unit argument at call sites (in `transformCall`). - Both sides must be stripped symmetrically so Erlang arity matching works. The old - workaround of appending `Value(UnitConstant)` in `bclType` was removed. -- **Block hoisting**: When `Let` bindings produce `Block [Match(...); body]` expressions, - these Blocks are invalid inside Erlang argument positions (Call, Apply, BinOp). The - `extractBlock` / `hoistBlocksFromArgs` / `wrapWithHoisted` helpers extract leading - assignments and hoist them before the enclosing expression. This fixes - match-in-expression patterns like `match x with ... |> equal "abc"`. -- **Recursive lambdas**: Self-recursive `let rec f x = ... f (x-1) ...` inside function - bodies generates Erlang named funs: `fun F(X) -> ... F(X-1) ... end` (OTP 17+). - Detected via `containsIdentRef` which checks if a lambda body references its own - binding ident. Mutual recursion (`let rec ... and ...`) is supported by bundling the - group into a single named fun dispatched by atom tag (`MutualRecBindings`). -- **String interpolation**: `$"text: {value}"` generates - `iolist_to_binary([<<"text: ">>, integer_to_binary(Value)])`. Integer values use - `integer_to_binary/1`, string values pass through, other types use - `io_lib:format("~p", [Value])`. Note: `io_lib:format` needs a charlist format string, - not a binary — uses `binary_to_list(<<"~p">>)` to convert. String concatenation from - Fable's Replacements (`string:concat`) is intercepted and replaced with - `iolist_to_binary([A, B])` since `string:concat` returns charlists, not binaries. - `to_string` conversion lives in `fable_string:to_string/1`. -- **Option representation**: `None` = `undefined` atom. Simple `Some(x)` is **erased** - (just `x`). Nested options (`Option>`), `GenericParam`, and `Any` types use - **wrapped** representation: `Some(x)` = `{some, x}`. This avoids ambiguity when - `Some(None)` would otherwise be indistinguishable from `None`. Runtime smart constructor - `fable_option:some/1` handles wrapping at generic call sites. Unlike JS/Python, `Unit` - does NOT need wrapping because Erlang's `ok` atom is distinct from `undefined`. -- **Object expressions / Interfaces**: `{ new IFoo with member _.Bar(x) = ... }` compiles - to an Erlang map of closures: `#{bar => fun(X) -> ... end}`. Property getters are stored - as evaluated values (not closures) since call sites use `Get(obj, FieldGet(name))` → - `maps:get(name, Obj)`. Interface method calls use `(maps:get(method, Obj))(Args)`. - Detection: `transformCall`'s `Get(calleeExpr, FieldGet, _, _)` branch checks if - `calleeExpr.Type` is a `DeclaredType` with `entity.IsInterface`. Self-referencing - members (e.g., `member x2.Test(i) = x2.Value - i`) are supported: when any member - references its `this` arg, the object is built behind a process-dict ref - (`ObjRef_N = make_ref()`, aliased to each self-ident, `put` the closure map, return the - ref) so closures can reach the object under construction. -- **ImportAll + Erase interface**: `[]` + `[]` interface pattern - for typed FFI bindings. `myModule.someMethod(args)` → `module:some_method(Args)`. - Detected in both `transformCall` (method calls) and `transformGet` (property access) by - matching `calleeExpr` as `Import` with `Selector = "*"`. Emits direct Erlang remote calls - instead of `fable_utils:iface_get` dispatch. Method names are converted via - `sanitizeErlangName` (camelCase → snake_case). Same pattern as JS/Python but with `:` call - syntax instead of attribute access. -- **`[]` → atoms**: `[]` means "a closed set of string-literal constants - for interop". On JS each case lowers to a string literal because that is what a JS API expects; - the Beam analogue is an **atom**, not a binary — the OTP functions such a binding targets (ETS - table types, `logger` levels, transport names, `gen_server` name registration) pattern-match - atoms and reject binaries. So on Beam a case compiles to a bare atom, quoted when the name is - not valid unquoted atom syntax (`[]` → `'Horizontal'`, - `CaseRules.KebabCase` → `'content-box'`). This makes `[]` and a plain nullary DU - equivalent on Beam — both are bare atoms — which is the intended end state. Consequences: - - `[]` cases are genuine bool/int/float constants rather than tags, - and keep their literal. An explicit `[]` on a case still wins. - - The type no longer maps to `Fable.String` on Beam (`makeType` in `FSharp2Fable.Util.fs`), so - `string x` routes through `fable_convert:to_string` (giving the atom's text as a binary) - instead of erasing to a no-op that would hand an atom to code expecting a binary. Ordered - comparison also becomes declaration-order correct, since union values now reach - `compare_union` (see the DU ordering section above). - - `[]` unions are deliberately *not* included: an erased case with no fields keeps its - binary. `[]` means "no runtime representation", not "a constant tag". - -- **Async/Task CE**: CPS (Continuation-Passing Style) implementation. `Async` is a function - `fun(Ctx) -> ok end` with context map `#{on_success, on_error, on_cancel, cancel_token}`. - No trampoline needed (Erlang has native TCO). `RunSynchronously` runs in same process - (not spawned) to preserve process dict access for mutable variables. `Parallel` spawns one - process per computation and collects results via message passing. Task CE is an alias — - Task builder methods route to `fable_async_builder`, `.Result` routes to `run_synchronously`. - Replacements routing: `FSharpAsyncBuilder`/`AsyncActivation` → `asyncBuilder`; - `FSharpAsync`/`AsyncPrimitives` → `asyncs`; `TaskBuilder`/`TaskBuilderBase` → `taskBuilder`; - `Task`/`Task` → `tasks`. `DefaultAsyncBuilder` in operators → `fable_async_builder:singleton`. -- **NewArray block hoisting**: `NewArray(ArrayValues ...)` uses `hoistBlocksFromArgs` + - `wrapWithHoisted` to hoist Let bindings out of array literal positions, matching the - pattern used for Call/Apply/BinOp arguments. -- **sprintf / printfn / String.Format**: Full F# format string support via `fable_string.erl` - runtime. `printf/1` parses format strings (`%d`, `%s`, `%.2f`, `%g`, `%x`, etc.) into a - continuation-based format object `#{input, cont}`. When a statically known argument type has a - custom `System.Object.ToString()` override, the compiler calls `printf/2` with a parallel list of - optional converters; the runtime applies them only to `%O`. `to_text` (sprintf), `to_console` - (printfn), `to_console_error` (eprintfn), `to_fail` (failwithf) apply continuations with - appropriate handlers. Multi-arity overloads (`to_text/1..5`) handle Fable's inlined arg - passing where `CurriedApply` flattens curried args into a single call. `format/2` handles - .NET `String.Format("{0} {1}", args)` with positional placeholders. Replacements routing: - `fsFormat` function handles `PrintfFormat.ctor` (→ `printf/1` or `printf/2`), `PrintFormatToString` - (→ `to_text`), `PrintFormatLine`/`PrintFormat` (→ `to_console`), etc. Dispatched from both - `operators` (for `ExtraTopLevelOperators.sprintf`) and `tryCall` (for `PrintfModule`/ - `PrintfFormat` entities). The old `toConsole` → `io:format("~s~n")` hack in Fable2Beam.fs - was removed. -- **MailboxProcessor**: In-process CPS continuation model, NOT gen_server. Agent state lives - in process dict keyed by `make_ref()`: `#{body, messages, continuation}`. `receive_msg` - uses `from_continuations` to store OnSuccess as pending continuation; `post` adds to queue - and calls `process_events`; `post_and_async_reply` creates a reply channel map - `#{reply => Fun}` where Fun stores the reply value in process dict, and the synchronous - CPS execution guarantees the value is available when `post` returns. `Reply` is dispatched - via `emitExpr` as `(maps:get(reply, $0))($1)`. Replacements route - `FSharpMailboxProcessor` and `FSharpAsyncReplyChannel` to the `mailbox` handler. -- **CurriedApply**: Uses a simple `List.fold` applying args one at a time: - `cleanArgs |> List.fold (fun fn arg -> Apply(fn, [arg])) cleanApplied`. This matches - JS (Fable2Babel.fs) and Python (Fable2Python.Transforms.fs). Never merge CurriedApply - args into Call nodes — that causes badarity errors when calling curried closures. -- **Erlang keyword escaping**: `sanitizeErlangName` pipes through `checkErlKeywords` at - the end to append `_` suffix to Erlang reserved words (e.g. `maybe` → `maybe_`, - `receive` → `receive_`). The `erlKeywords` set covers OTP 25+ keywords including - `maybe` and `else`. This is needed because F# identifiers like `maybe` are valid but - would generate Erlang syntax errors. -- **Arrays as process-dict refs**: All non-byte arrays are now process-dict refs - (`make_ref()` + `put/get`), enabling cross-function mutation. `NewArray` wraps with - `fable_utils:new_ref([...])`. `derefArr`/`wrapArr` helpers in Replacements convert - between refs and plain lists for bulk operations. Binary comparison operators on arrays - use `fable_comparison:compare(A, B) op 0`. TypeTest for arrays uses `is_reference`. - When an array *literal* flows directly into an FFI/Emit binding that derefs its - argument (e.g. `maps:from_list(erlang:get($0))`), the naive output is - `...erlang:get(fable_utils:new_ref([...]))` — a pointless process-dict round-trip - on an immutable literal (it also leaks an un-erased process-dict entry). - `simplifyArrayRefDerefs` in `Fable2Beam.fs` cancels it on the Beam AST when - building an `Emit` node: if an argument is an inline `new_ref(list)` and *every* - occurrence of its `$N` placeholder in the macro template is deref-wrapped - (`erlang:get($N)`/`get($N)`), the deref is dropped from the template and the - underlying list is passed directly. Gating on the argument's AST shape (rather than - parsing rendered Erlang) keeps it robust; a ref-typed *variable* (a bound/mutable - array) keeps its deref. -- **Byte arrays via atomics**: Byte arrays (`Array`) use Erlang's `atomics` module - for true O(1) mutable read/write. Represented as `{byte_array, Size, AtomicsRef}` tuples. - Runtime helpers in `fable_utils.erl` handle both direct tuples and process-dict - ref-wrapped byte arrays: `byte_array_get/2`, `byte_array_set/3`, `byte_array_length/1`. - `Array.zeroCreate` uses `new_byte_array_zeroed` (atomics are zero by default). - `Array.create n v` uses `new_byte_array_filled` (avoids intermediate list allocation). - This enabled a 2048x2048 raytracer demo to run at ~40s on BEAM with no hand-patches. -- **Mutable variable ref erasure**: Process dict refs for mutable `let` bindings are erased - at scope exit (`erase(Key)`) to prevent leaks. The `EraseMutableRefs` expression is emitted - at the end of function bodies and appended to Block expressions containing mutable bindings. -- **Stray ok atom removal**: The ErlangPrinter strips bare `ok` atoms (F# unit values) from - non-final positions in all body contexts: top-level functions, fun/named-fun clauses, case - clauses, try/catch bodies, and block expressions. Matches both `Literal(AtomLit "ok")` and - `Emit("ok", [])` forms (the latter from `System.Object..ctor`). -- **CancellationToken**: Process dict pattern with `make_ref()` storing - `#{cancelled, listeners, next_id}`. Supports `create`, `cancel`, `cancel_after` (timer-based), - `register`/`unregister`, `is_cancellation_requested` (drains cancel messages from mailbox). - Sleep integration uses `receive` instead of `timer:sleep` when token is present. -- **Stopwatch**: Runtime `fable_stopwatch.erl` using `erlang:monotonic_time(microsecond)`. - Supports `StartNew`, `Start`, `Stop`, `Reset`, `Restart`, `Elapsed`, `ElapsedMilliseconds`, - `IsRunning`, `Frequency`, `GetTimestamp`. -- **FSharp.Reflection**: Full runtime support via `fable_reflection.erl` + compile-time type - info generation in `Fable2Beam.Reflection.fs`. TypeInfo = Erlang map with `fullname`, - `generics`, and optional `fields` (records) or `cases` (unions). PropertyInfo/CaseInfo are - maps with `name`, `typ`, `tag`, `fields`. Reflection functions renamed to avoid Erlang BIF - clashes: `is_tuple` → `is_tuple_type`, `is_function` → `is_function_type`. Union tag matching - uses atom tags via `erl_tag` field (matching Beam's `{atom_tag, Field1, Field2}` representation). `MakeRecord`/ - `MakeUnion` handle both plain lists and process-dict ref arrays. `GetRecordFields` resolves - concrete types through `TypeCast` wrappers via `getConcreteType` helper. -- **Async error wrapping**: `wrap_error/1` in `fable_async.erl` normalizes raw errors so - `.Message` accessor works: raw binaries (from `failwith`) → `#{message => Bin}`, maps - (from `raise (exn ...)`) pass through, refs pass through, everything else → formatted map. - Used in `catch_async`, `try_with`, and `try_finally`. `catch_async` uses atom tags - `{choice1_of2, V}` / `{choice2_of2, wrap_error(E)}` matching Beam's Choice union representation. -- **OperationCanceledException**: Added to the exception type pattern in Beam Replacements - alongside `BuiltinSystemException` and `KeyNotFoundException`. -- **Module-level mutable variables**: `let mutable x = v` at module level is routed through - the process dictionary (same mechanism as local mutable `let` bindings). The process-dict - key is namespaced by the emitting Erlang module name — `_` (e.g. `x` in module - `foo` → key `foo_x`) — so identically-named mutables in different modules don't collide in - the shared, process-local process dictionary. The key is built by the single `mutableStateKey` - helper (`Fable2Beam.Util.fs`) and used at every site so reads, writes and the initialiser - always agree. The declaration emits a `main/0` fragment that initialises the value - (`put(foo_x, v)`); reads of the ident emit `get(foo_x)` and writes (`x <- e`) emit - `put(foo_x, e)` (see the `IdentExpr`/`Set` branches and the `MemberDeclaration` value case in - `Fable2Beam.fs`). All `main/0` fragments — mutable inits, - snapshot inits (below), and `do` actions — are merged in declaration order, so module - initialisation runs as a single ordered sequence, mirroring F#. A value initialiser that - lowers to a multi-statement block (e.g. it contains a `let`) is stored as `put(x, )` - using the *whole* block (its final expression is the value), and is wrapped in an - immediately-invoked `fun` so its local Erlang variables stay isolated — Erlang `begin...end` - does not introduce a scope, so two initialisers reusing the same local name would otherwise - clash in the shared `main/0` clause. - - **Snapshotting immutable values that read a mutable**: an *immutable* module value whose - initializer reads a module-level mutable (e.g. `let c = topA`) must capture the value at - binding time, because F# evaluates module bindings once, in order, before any later - reassignment. Compiled naively as a lazy 0-arity accessor it would re-read the *live* - process-dict value and observe later writes. Such values are therefore also eagerly - initialised in `main/0` (`put(foo_c, )`, using the same module-namespaced key and - emitted in declaration order so it captures the mutable's value at that point) plus an - accessor — named by the bare value name — that reads the snapshot (`c() -> get(foo_c)`). - Detection is `readsFreeMutable`, which walks the Fable body tracking locally - bound names and triggers only on a *free* (module-level) mutable reference — - self-contained bodies whose only mutables are local stay lazy, so they don't gain a - spurious dependency on `main/0`. - - **Module init runs before tests**: the Erlang test runner (`erl_test_runner.erl`) calls - `test_*/0` functions directly and would never run `main/0`, so module-level - initialisation (mutable inits and `do` actions) would never execute and reads would - return `undefined`. The runner now invokes each module's `main/0` (if exported) before - its tests, mirroring .NET module initialisation (which runs before any module code). - This is safe because Beam test modules contain no top-level side effects beyond these - initialisers. -- **Function reference identity** (`LanguagePrimitives.PhysicalEquality` / `ReferenceEquals`): - the *same* F# function value is represented by different Erlang funs at different sites — - uncurried (a single N-arity fun), re-curried (`fun(A0) -> fun(A1) -> F(A0, A1) end end`, from a - `Curry` node), or wrapped in an uncurrying (eta) adapter to fill an uncurried slot. Erlang - compares funs by closure identity, so these are never `=:=` even though they denote one value — - which made `PhysicalEquality` wrongly return `false` (e.g. a stored callback could never be - removed by identity). Fix: both adapter kinds are built through tagged helpers in `fable_utils` — - `make_curry/2` (routed from the `Curry` node in `Fable2Beam.fs`) and `make_eta/2` (routed from - the `EtaAdapterDelegate` pattern) — each capturing an underlying `F` inside a - `{fable_curry_adapter, F, N}` / `{fable_eta_adapter, F, N}` marker as the outermost fun's ONLY - captured variable. `fun_ref_eq/2` then peels those markers via `erlang:fun_info(F, env)` before - `=:=`, normalising every representation of a value to the same `F`. Both helpers apply `F` exactly - as the default lowering would (`make_eta` via `apply_curried`, `make_curry` via `erlang:apply` — - all args at once, matching the multi-arg `Apply`), so they are behaviour-identical bar the marker. - Key points: - - **Adapter cancellation** (mirrors the Python target's `curry.py` memoization, where - `curry(uncurry(g))` returns the cached original `g`): if `make_eta`/`make_curry` receives a - value that is already the *opposite*, same-arity adapter, it returns the original underlying - fun instead of stacking a second wrapper — `uncurry ∘ curry = curry ∘ uncurry = id`. A - round-tripped value is then literally the *same* fun (native `=:=` holds with no unwrap and no - wrapper cost), and adapters never accumulate. The marker travels inside the closure env, so - unlike Python's process-local `WeakKeyDictionary` this survives a fun being sent between - processes. The arity in the marker guards the cancellation (a mismatch wraps instead of - cancelling — no `badarity`). `unwrap_fun` still covers the non-round-trip case (a value stored - in one representation and compared against the other). - - This is **precise reference identity, not structural equality**: only Fable's own - compiler-generated adapters carry a marker, so a hand-written eta expansion (`fun x -> g x`) - or a partial application (which captures the collected args, so its outermost fun has >1 - captured variable) is left intact and stays distinct — matching .NET. An earlier version used - a *shape heuristic* (unwrap any 1-arity fun capturing a single function) which wrongly - collapsed `fun x -> g x` onto `g`; the marker approach replaces it. Regression-guarded by - `test PhysicalEquality distinguishes eta expansion from the original`. - - **Arity cap 2..7** (matching `make_eta`). Functions with 8+ curried args fall back to the - default lowering and are not identity-preserved; extend the `make_curry_marked`/ - `make_eta_marked` clauses if needed. - - **Generic call sites**: routing to `fun_ref_eq` is gated on the operand being a - `LambdaType`/`DelegateType` at the call site (`isFunctionType` in `Beam/Replacements.fs`). A - value typed as a bare generic `'T` (Fable does not monomorphise) falls back to plain `=:=`, so - wrapping `PhysicalEquality` in a generic helper does not get the function-aware path. - - **Correctness over performance**: `make_curry` sits on the pervasive currying path and adds a - marker allocation + `erlang:apply` per call versus the inline nested lambdas — but this is the - same shape of incremental-application cost the Python target already pays (`_curry_n` / - `_uncurry_n`), and adapter cancellation removes it entirely on round-trips. Any residue is an - accepted alpha-stage trade-off — a passing unit test wins over the micro-cost. - -## Future Improvements - -### Module-level mutable state: per-process, requires `main/0` - -Module-level mutables live in the **process dictionary** and are initialised by `main/0`. -That makes their semantics correct only when `main/0` actually runs in the process that later -reads the state: - -- **Works**: entry-point programs (the `main/0` Fable emits is the program entry — quicktest, - real apps) and the test harness (now calls `main/0` before each module's tests). -- **Does not work**: a *library* module whose functions are called by other code without that - module's `main/0` having run — its module-level mutables/snapshots read `undefined`. Reads - from a **different process** than the one that ran `main/0` also see `undefined`, since the - process dictionary is process-local. - -Keys are namespaced by module (`_`), so multiple modules' mutables with the same -name running in the **same** process no longer collide — the remaining limitation is purely the -process-locality above, not naming. - -This matches the broader Beam design (mutation is single-process by design; see "Class instance -representation" and "Mutable Collections" above), but it means module-level mutable global state -is not a fully general feature. Follow-up options if true cross-process / load-time module state -is ever needed: - -- **`persistent_term`** + `-on_load` for global, load-time initialisation (reads visible from - any process). Downside: writes are global-GC-heavy and meant for write-rarely data, so - frequently-mutated module values would be slow. -- **ETS** table per module for shared mutable state — O(1) writes, but cross-process shared - state, against the isolation model. - -Neither is implemented; the current per-process approach is the right default for typical F# -programs where module-level mutables are entry-point/program state. - -### Mutable Collections: Process Dict vs ETS - -Currently, `Dictionary`, `HashSet`, `ResizeArray`, and `Array` use the process dictionary -pattern: `make_ref()` + `put(Ref, EntireCollection)` / `get(Ref)`. Every mutation replaces -the entire map or list — adding one key to a 10,000-entry Dictionary copies all 10,000 -entries (O(N) per mutation). - -**ETS (Erlang Term Storage)** is an alternative that provides O(1) in-place mutable tables. -However, the current process dict approach is arguably the **better design** for BEAM: - -- **Process isolation preserved**: Mutations stay within a single process, matching the - BEAM philosophy of isolated processes with no shared mutable state -- **No accidental sharing**: ETS tables are cross-process by default, which would break - the isolation model that makes BEAM reliable -- **Simple cleanup**: `erase(Ref)` at scope exit vs explicit `ets:delete(Tab)` with no - finalizers to ensure cleanup -- **Small collections are fine**: Most F# code uses small-to-medium collections where - the full-copy overhead is negligible - -| Aspect | Process Dict (current) | ETS | +| Priority | Gap | Suggested direction | | --- | --- | --- | -| Read | O(1) `get(Ref)` + O(1) `maps:get` | O(1) `ets:lookup` | -| Write | O(N) full map copy via `put` | O(1) `ets:insert` | -| Process isolation | Yes (process-local) | No (shared by default) | -| Small collections | Fast (no table overhead) | Slower (table creation cost) | -| Large collections | Slow (full copy per mutation) | Fast (in-place mutation) | -| Cleanup | `erase(Ref)` | `ets:delete(Tab)` (must be explicit) | - -**Conclusion**: The process dict approach is the right default. ETS could be offered as -an opt-in optimization (e.g., via an attribute) for specific cases where large collection -mutation performance is critical, but it should not be the default since it introduces -shared mutable state — exactly what BEAM is designed to avoid. - -## Open Questions - -- ~~**Records vs Maps**: Erlang records are compile-time tuples (fast, but rigid). - Maps are dynamic (flexible, slower). For F# records, maps seem more natural.~~ - **Decided**: Erlang maps. Field names as snake_case atoms, `maps:get/2` for access, - `maps:put/3` for update. Structural equality via native `=:=`. -- **OTP project structure**: Generate a full OTP application structure with - `rebar3`? Or just standalone `.erl` files initially? -- ~~**Interop**: How should F# code call existing Erlang/Elixir libraries? - Fable.Core attributes like `[]`?~~ - **Decided**: Three interop mechanisms: (1) `[]` for individual - function imports → `module:func(Args)`, (2) `[]` for inline - Erlang expressions, (3) `[]` + `[]` interface for typed - module bindings → `module:method(Args)`. The ImportAll pattern mirrors JS/Python but - emits Erlang remote calls instead of attribute access. -- ~~**Testing**: Use EUnit, Common Test, or just assert in generated code?~~ - **Decided**: xUnit with `[]` on .NET side, `Fable.Core.Testing.Assert` - when compiled to BEAM. Same pattern as Python/Rust targets. -- **Integer wrapping granularity**: Wrap every arithmetic operation, or only at - let-binding boundaries? Former is safer, latter is faster. -- ~~**Function name encoding**: Erlang atom names from double-backtick F# names - currently produce URL-encoded names (e.g. `test$0020_add`). Need to decide - on a cleaner encoding or stick with snake_case conversion.~~ - **Decided**: `sanitizeErlangName` decodes `$XXXX` hex sequences, strips apostrophes, - converts to snake_case, collapses underscores. See "Decisions Made" above. - -## Prior Art - -- **Caramel** — OCaml → Erlang compiler (abandoned, but useful reference for - ML-to-BEAM type mappings): -- **Gleam** — Typed functional language on BEAM, compiles to Erlang. Study its - compiler for BEAM code generation patterns: -- **LFE** (Lisp Flavored Erlang) — another language targeting BEAM, shows OTP - integration from non-Erlang language: -- **Fable.Python** — the direct template for this work. Same architecture, similar - challenges (dynamic target language, no classes for DUs, module-per-file) +| P2 | Mutable list/map storage copies on update | Benchmark realistic collection sizes. Keep process isolation as the default; add an explicit ETS-backed type or optimization only for measured hot paths. | +| P2 | Curry/eta identity adds adapter work | Benchmark generated functions and extend marked adapters beyond arity 7 only when required by real code. | +| P2 | Generated Erlang has no published size/runtime baseline | Track compile time, BEAM file size, startup, allocation, collection workloads, Async, and message-heavy applications. | +| P2 | Application-scale evidence is private and not reproducible in this repository | Keep the downstream application as a compatibility signal and add a public packaged-compiler smoke test using Fable.Beam/Fable.Actor. | + +### Ecosystem work + +- Keep OTP bindings in Fable.Beam rather than adding OTP-specific abstractions to the + compiler. +- Maintain a compatibility matrix for Fable, Fable.Beam, Fable.Actor, Erlang/OTP, + and rebar3. +- Add end-to-end examples for a command-line program, a rebar3 library, an OTP + application, supervised processes, and distribution. +- Test generated modules as dependencies of hand-written Erlang applications, not + only as application entry points. +- Document the public Erlang ABI for unions, records, options, exceptions, + functions, and mutable values before external packages depend on it. + +### Suggested beta criteria + +- There is no known silent miscompilation, data corruption, or process-safety defect + in the documented supported surface. +- Remaining semantic and API gaps are deliberate, documented exclusions and have + regression tests where practical; complete .NET parity is not required. +- Supported constructs do not silently compile to an `unsupported_*` placeholder. + Statically detectable unsupported constructs produce an actionable diagnostic. +- The complete BEAM suite passes on the oldest and newest supported OTP releases. +- Packaged `dotnet fable --lang beam` output builds and runs in a clean consumer + project without repository-local files. +- Module initialization and mutation have documented, tested behavior for the + supported executable, library, and spawned-process scenarios. +- The generated-value ABI used by supported interop scenarios is documented, and + representation changes are called out. +- At least one non-trivial downstream OTP application remains green, with a public + packaged-consumer smoke test covering the reproducible integration path. + +### Suggested stable-target criteria + +- No known high-severity semantic divergence remains in supported F# language + features or core runtime APIs. +- F#/.NET parity exclusions are reviewed, documented, and small enough to form a + deliberate support policy. +- The Erlang ABI has a compatibility and versioning policy. +- OTP, rebar3, Fable.Beam, and Fable.Actor compatibility ranges are published and + exercised in CI. +- Diagnostics fail early for unsupported code and point to a documented alternative. +- Performance baselines show no blocking regressions in representative compute, + collection, Async, and OTP workloads. +- Release, upgrade, and deprecation procedures have been exercised across multiple + Fable releases and real consumer applications. diff --git a/tests/Beam/ConversionTests.fs b/tests/Beam/ConversionTests.fs index 896bbd54cf..23c14a0c0c 100644 --- a/tests/Beam/ConversionTests.fs +++ b/tests/Beam/ConversionTests.fs @@ -938,12 +938,11 @@ let ``test BitConverter.ToUInt32 works`` () = let bytes = BitConverter.GetBytes(value) BitConverter.ToUInt32(bytes, 0) |> equal value -// TODO: UInt64 values > Int64.MaxValue — Fable compiles UInt64 as Int64, round-trip fails -// [] -// let ``test BitConverter.ToUInt64 works`` () = -// let value = 0xFF02030405060708UL -// let bytes = BitConverter.GetBytes(value) -// BitConverter.ToUInt64(bytes, 0) |> equal value +[] +let ``test BitConverter.ToUInt64 works above Int64.MaxValue`` () = + let value = 0xFF02030405060708UL + let bytes = BitConverter.GetBytes(value) + BitConverter.ToUInt64(bytes, 0) |> equal value [] let ``test BitConverter.ToSingle works`` () = From 682b235bbfdb81e1bccfac7a0ac4680d27448e65 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 19:54:48 +0200 Subject: [PATCH 101/322] fix(beam): preserve unmatched filtered exceptions (#5001) --- src/Fable.Transforms/Beam/Beam.AST.fs | 8 +- src/Fable.Transforms/Beam/ErlangPrinter.fs | 6 +- src/Fable.Transforms/Beam/FABLE-BEAM.md | 12 ++- src/Fable.Transforms/Beam/Fable2Beam.Util.fs | 39 +++++++ src/Fable.Transforms/Beam/Fable2Beam.fs | 103 ++++++++++++++----- src/Fable.Transforms/Beam/Replacements.fs | 33 ++++-- src/fable-library-beam/fable_utils.erl | 47 +++++++++ tests/Beam/MiscTests.fs | 15 +++ tests/Beam/SeqExpressionTests.fs | 36 +++---- tests/Beam/TryCatchTests.fs | 22 ++++ 10 files changed, 265 insertions(+), 56 deletions(-) diff --git a/src/Fable.Transforms/Beam/Beam.AST.fs b/src/Fable.Transforms/Beam/Beam.AST.fs index dec6df7873..3147f58624 100644 --- a/src/Fable.Transforms/Beam/Beam.AST.fs +++ b/src/Fable.Transforms/Beam/Beam.AST.fs @@ -35,7 +35,13 @@ type ErlExpr = | Block of exprs: ErlExpr list | BinOp of op: string * left: ErlExpr * right: ErlExpr | UnaryOp of op: string * operand: ErlExpr - | TryCatch of body: ErlExpr list * catchVar: string * catchBody: ErlExpr list * after: ErlExpr list + | TryCatch of + body: ErlExpr list * + catchClassVar: string * + catchReasonVar: string * + catchStackVar: string * + catchBody: ErlExpr list * + after: ErlExpr list | Emit of template: string * args: ErlExpr list | Receive of clauses: ErlCaseClause list * after: (ErlExpr * ErlExpr list) option diff --git a/src/Fable.Transforms/Beam/ErlangPrinter.fs b/src/Fable.Transforms/Beam/ErlangPrinter.fs index 3be132da7b..ccf36b555c 100644 --- a/src/Fable.Transforms/Beam/ErlangPrinter.fs +++ b/src/Fable.Transforms/Beam/ErlangPrinter.fs @@ -407,7 +407,7 @@ module Output = complexExprWithParens sb indent expr - | TryCatch(body, catchVar, catchBody, after) -> + | TryCatch(body, catchClassVar, catchReasonVar, catchStackVar, catchBody, after) -> sb.AppendLine("try") |> ignore let tryBody = stripNoEffect body @@ -427,7 +427,9 @@ module Output = writeIndent () sb.AppendLine($"catch") |> ignore writeIndent () - sb.AppendLine($" _:%s{catchVar} ->") |> ignore + + sb.AppendLine($" %s{catchClassVar}:%s{catchReasonVar}:%s{catchStackVar} ->") + |> ignore let catchBody' = stripNoEffect catchBody diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 4df00964d1..19e8db2d7f 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -337,6 +337,16 @@ process-dictionary-backed mutable F# object to another process and expect shared-object semantics; treat a byte array passed between processes as explicitly shared mutable state. +### Exceptions + +F# and built-in .NET exceptions use maps with a nominal `exn_type` atom. Built-in +type tests follow the .NET exception hierarchy, so typed handlers distinguish sibling +exceptions while accepting derived exceptions through their base type. + +Generated Erlang catches the native `Class:Reason:Stacktrace` triple. `reraise` and +unmatched filtered handlers use `erlang:raise/3`, preserving the original Erlang +exception class, reason, and stacktrace. + ### Async, Task, and MailboxProcessor `Async<'T>` is a cold continuation-passing function: @@ -454,7 +464,6 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Exceptions | Filtered handlers do not yet rethrow every unmatched exception correctly. | | Numeric APIs | Some byref `TryParse`/`DivRem` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, integer `Log2`, and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | @@ -493,7 +502,6 @@ visible rather than silent correctness failures. | P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | | P0 | Object-model gaps affect valid F# | Fix silent wrong-code paths in the claimed object-model surface. Keep unsupported class and struct forms as explicit exclusions until implemented. | | P0 | Numeric and byref APIs have correctness gaps | Fix incorrect results in claimed numeric APIs. Missing APIs can remain documented exclusions; restore regression tests as implementations land. | -| P0 | Exception filters can swallow unmatched errors | Preserve Erlang class, reason, and stacktrace and re-raise unchanged when no F# handler matches. | ### Fidelity and diagnostics diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs index 501518a8e4..74f64edefb 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs @@ -436,6 +436,45 @@ let wrapWithHoisted (hoisted: Beam.ErlExpr list) (expr: Beam.ErlExpr) : Beam.Erl let atomLit name = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom name)) +/// Whether generated Erlang still reads a variable. This is intentionally checked after +/// lowering: a Fable catch identifier used only by `reraise` becomes the raw +/// class/reason/stacktrace variables and does not need a separately wrapped exception value. +let rec referencesVariable (name: string) (expr: Beam.ErlExpr) : bool = + let anyReferences exprs = + exprs |> List.exists (referencesVariable name) + + match expr with + | Beam.ErlExpr.Variable varName -> varName = name + | Beam.ErlExpr.Tuple elements + | Beam.ErlExpr.List elements + | Beam.ErlExpr.Block elements -> anyReferences elements + | Beam.ErlExpr.ListCons(head, tail) + | Beam.ErlExpr.BinOp(_, head, tail) -> referencesVariable name head || referencesVariable name tail + | Beam.ErlExpr.Map entries -> + entries + |> List.exists (fun (key, value) -> referencesVariable name key || referencesVariable name value) + | Beam.ErlExpr.Call(_, _, args) -> anyReferences args + | Beam.ErlExpr.Apply(func, args) -> referencesVariable name func || anyReferences args + | Beam.ErlExpr.Fun clauses + | Beam.ErlExpr.NamedFun(_, clauses) -> + clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + | Beam.ErlExpr.Case(value, clauses) -> + referencesVariable name value + || clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + | Beam.ErlExpr.Match(_, value) + | Beam.ErlExpr.UnaryOp(_, value) -> referencesVariable name value + | Beam.ErlExpr.TryCatch(body, _, _, _, catchBody, after) -> + anyReferences body || anyReferences catchBody || anyReferences after + | Beam.ErlExpr.Emit(_, args) -> anyReferences args + | Beam.ErlExpr.Receive(clauses, after) -> + clauses + |> List.exists (fun clause -> anyReferences clause.Guard || anyReferences clause.Body) + || (after + |> Option.exists (fun (timeout, body) -> referencesVariable name timeout || anyReferences body)) + | Beam.ErlExpr.Literal _ -> false + let rec private patternBinds (pattern: Beam.ErlPattern) : bool = match pattern with | Beam.PVar _ -> true diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 7d1166db91..553bd13cb8 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -92,7 +92,8 @@ type Context = CtorFieldExprs: Map // field name -> Erlang expr during constructor ClassFieldPrefix: bool // When true, NewRecord uses "field_" prefix for map keys (for explicit val field class ctors) CtorParamNames: Set // Constructor parameter names (stored as class fields) - CatchReasonVar: (string * string) option // (catch ident name, Erlang reason var name) for reraise support + // invariant: CatchVars retains class, reason, and stacktrace so reraise preserves native exception identity. + CatchVars: (string * string * string * string) option } /// Check if an entity ref refers to an interface type @@ -881,16 +882,18 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er match catch_, afterExprs with | Some(ident, catchExpr), _ -> // Create a temp var for the raw reason, then bind the catch ident. - // Custom exceptions (maps with __type) are preserved as-is. + // Custom exceptions (maps with exn_type) are preserved as-is. // Plain errors (binaries from failwith, atoms, etc.) are wrapped in #{message => ...}. let ctr = com.IncrementCounter() + let classVar = $"_Exn_class_%d{ctr}" let reasonVar = $"Exn_reason_%d{ctr}" + let stackVar = $"_Exn_stack_%d{ctr}" let identVar = capitalizeFirst ident.Name let ctx' = { ctx with LocalVars = ctx.LocalVars.Add(ident.Name) - CatchReasonVar = Some(ident.Name, reasonVar) + CatchVars = Some(ident.Name, classVar, reasonVar, stackVar) } let erlCatchBody = transformExpr com ctx' catchExpr @@ -900,10 +903,10 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Beam.ErlExpr.Block es -> es | e -> [ e ] - // Only generate the exception wrapping/binding when the catch body - // actually references the exception identifier. This avoids unused - // term warnings from the Erlang compiler. - if containsIdentRef ident.Name catchExpr then + // Only generate the exception wrapping/binding when the lowered catch body still + // reads it. A source-level `reraise` references the Fable catch identifier, but it + // lowers directly to erlang:raise/3 with the raw catch variables. + if catchBodyExprs |> List.exists (referencesVariable identVar) then let reasonRef = Beam.ErlExpr.Variable reasonVar let formatExpr = @@ -977,22 +980,41 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er ) ) - Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, [ bindIdent ] @ catchBodyExprs, afterExprs) + Beam.ErlExpr.TryCatch( + bodyExprs, + classVar, + reasonVar, + stackVar, + [ bindIdent ] @ catchBodyExprs, + afterExprs + ) else - Beam.ErlExpr.TryCatch(bodyExprs, reasonVar, catchBodyExprs, afterExprs) + Beam.ErlExpr.TryCatch(bodyExprs, classVar, reasonVar, stackVar, catchBodyExprs, afterExprs) | None, [] -> // No catch handler and no finalizer erlBody | None, _ -> // No catch handler but has finalizer (use binding) → try/after let ctr = com.IncrementCounter() + let classVar = $"_Exn_class_%d{ctr}" let reasonVar = $"Exn_reason_%d{ctr}" + let stackVar = $"_Exn_stack_%d{ctr}" // Re-throw in catch to preserve the error after running the after block Beam.ErlExpr.TryCatch( bodyExprs, + classVar, reasonVar, + stackVar, [ - Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ]) + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) ], afterExprs ) @@ -1301,19 +1323,39 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Throw(Some exprArg, _typ) -> // For reraise: if the thrown expression references the catch ident, // use the raw Erlang reason variable to preserve the original exception. - match exprArg, ctx.CatchReasonVar with - | IdentExpr ident, Some(catchIdentName, reasonVar) when ident.Name = catchIdentName -> - Beam.ErlExpr.Call(Some "erlang", "error", [ Beam.ErlExpr.Variable reasonVar ]) + match exprArg, ctx.CatchVars with + | IdentExpr ident, Some(catchIdentName, classVar, reasonVar, stackVar) when ident.Name = catchIdentName -> + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) | _ -> let erlExpr = transformExpr com ctx exprArg Beam.ErlExpr.Call(Some "erlang", "error", [ erlExpr ]) | Throw(None, _typ) -> - // Re-raise (should not normally happen outside catch context) - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "rethrow")) ] - ) + match ctx.CatchVars with + | Some(_, classVar, reasonVar, stackVar) -> + Beam.ErlExpr.Call( + Some "erlang", + "raise", + [ + Beam.ErlExpr.Variable classVar + Beam.ErlExpr.Variable reasonVar + Beam.ErlExpr.Variable stackVar + ] + ) + | None -> + // Re-raise should not occur outside a catch context. + Beam.ErlExpr.Call( + Some "erlang", + "error", + [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "rethrow")) ] + ) | Curry(e, arity) when arity >= 2 && arity <= 7 @@ -1994,13 +2036,28 @@ and transformTest (com: IBeamCompiler) (ctx: Context) (kind: TestKind) (expr: Ex Beam.ErlExpr.BinOp("andalso", isRef, isList) | Fable.AST.Fable.Type.Tuple _ -> Beam.ErlExpr.Call(None, "is_tuple", [ erlExpr ]) | Fable.AST.Fable.Type.DeclaredType(ref, _) -> - // For exception types, check __type tag: (is_map(X) andalso maps:get(__type, X, undefined) =:= type_name) + // For exception types, check the nominal exn_type tag. // For other types, check is_map or is_reference (class instances use refs) let isMap = Beam.ErlExpr.Call(None, "is_map", [ erlExpr ]) let isRef = Beam.ErlExpr.Call(None, "is_reference", [ erlExpr ]) - match com.TryGetEntity(ref) with - | Some entity when entity.IsFSharpExceptionDeclaration -> + match ref.FullName, com.TryGetEntity(ref) with + | Replacements.Util.BuiltinSystemException typeName, _ -> + Beam.ErlExpr.Call( + Some "fable_utils", + "is_exception_type", + [ erlExpr; atomLit (sanitizeErlangName typeName) ] + ) + | ("System.Exception" | "System.AggregateException" | "System.Collections.Generic.KeyNotFoundException" | "System.OperationCanceledException"), + _ -> + let typeName = ref.FullName.Substring(ref.FullName.LastIndexOf('.') + 1) + + Beam.ErlExpr.Call( + Some "fable_utils", + "is_exception_type", + [ erlExpr; atomLit (sanitizeErlangName typeName) ] + ) + | _, Some entity when entity.IsFSharpExceptionDeclaration -> let typeName = sanitizeErlangName entity.DisplayName let typeCheck = @@ -3705,7 +3762,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = CtorFieldExprs = Map.empty ClassFieldPrefix = false CtorParamNames = Set.empty - CatchReasonVar = None + CatchVars = None } let ctorNameRegistry = System.Collections.Generic.Dictionary() diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 078f4107b8..f93e9cfcbf 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5954,10 +5954,16 @@ let tryCall | _ -> None | "System.Exception" -> match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some - | ".ctor", None, [ msg; inner ] -> emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some + | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{exn_type => exception, message => $0}" |> Some + | ".ctor", None, [ msg; inner ] -> + emitExpr r t [ msg; inner ] "#{exn_type => exception, message => $0, inner_exception => $1}" + |> Some | ".ctor", None, [] -> - emitExpr r t [] "#{message => <<\"Exception of type 'System.Exception' was thrown.\">>}" + emitExpr + r + t + [] + "#{exn_type => exception, message => <<\"Exception of type 'System.Exception' was thrown.\">>}" |> Some | "get_Message", Some c, _ -> // Handle both map exceptions and reference-based class exceptions @@ -5980,21 +5986,30 @@ let tryCall | BuiltinSystemException _ | "System.Collections.Generic.KeyNotFoundException" | "System.OperationCanceledException" -> + let typeName = info.DeclaringEntityFullName + + let typeTag = + typeName.Substring(typeName.LastIndexOf('.') + 1) |> sanitizeErlangName + match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some + | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some | ".ctor", None, [ msg; second ] -> match info.SignatureArgTypes with // (message, paramName): paramName is not modelled, only the message is kept - | [ _; String ] -> emitExpr r t [ msg ] "#{message => $0}" |> Some + | [ _; String ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some // (message, innerException) - | _ -> emitExpr r t [ msg; second ] "#{message => $0, inner_exception => $1}" |> Some + | _ -> + emitExpr r t [ msg; second ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" + |> Some // (message, paramName, innerException) | ".ctor", None, [ msg; _paramName; inner ] -> - emitExpr r t [ msg; inner ] "#{message => $0, inner_exception => $1}" |> Some + emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" + |> Some | ".ctor", None, [] -> - let typeName = info.DeclaringEntityFullName let msg = $"Exception of type '%s{typeName}' was thrown." - emitExpr r t [] $"#{{message => <<\"%s{msg}\">>}}" |> Some + + emitExpr r t [] $"#{{exn_type => %s{typeTag}, message => <<\"%s{msg}\">>}}" + |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some | _ -> None diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index 6f13d5af3c..78d1f8337a 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -25,6 +25,7 @@ to_list/1, enumerate_to_list/1, aggregate_exception/2, + is_exception_type/2, new_byte_array/1, new_byte_array_zeroed/1, new_byte_array_filled/2, @@ -65,6 +66,7 @@ -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). -spec aggregate_exception(binary(), term()) -> map(). +-spec is_exception_type(term(), atom()) -> boolean(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). -spec new_byte_array_zeroed(non_neg_integer()) -> tuple(). -spec new_byte_array_filled(non_neg_integer(), non_neg_integer()) -> tuple(). @@ -467,11 +469,56 @@ aggregate_exception(BaseMessage, ExceptionsValue) -> | [[<<" (">>, exception_message(Exception), <<")">>] || Exception <- Exceptions] ]), #{ + exn_type => aggregate_exception, message => Message, inner_exception => InnerException, inner_exceptions => new_ref(Exceptions) }. +%% Built-in .NET exceptions carry a compact nominal tag. Follow the .NET exception +%% hierarchy here so a catch for a base exception also accepts its derived exceptions. +is_exception_type(Exception, Expected) when is_reference(Exception) -> + is_exception_type(get(Exception), Expected); +is_exception_type(#{exn_type := Actual}, Expected) -> + exception_type_is(Actual, Expected); +is_exception_type(#{message := _}, exception) -> + true; +is_exception_type(_, _) -> + false. + +exception_type_is(Type, Type) -> true; +exception_type_is(_, exception) -> true; +exception_type_is(application_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(argument_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(argument_null_exception, Expected) -> + exception_type_is(argument_exception, Expected); +exception_type_is(argument_out_of_range_exception, Expected) -> + exception_type_is(argument_exception, Expected); +exception_type_is(arithmetic_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(divide_by_zero_exception, Expected) -> + exception_type_is(arithmetic_exception, Expected); +exception_type_is(not_finite_number_exception, Expected) -> + exception_type_is(arithmetic_exception, Expected); +exception_type_is(overflow_exception, Expected) -> exception_type_is(arithmetic_exception, Expected); +exception_type_is(aggregate_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(key_not_found_exception, Expected) -> exception_type_is(system_exception, Expected); +exception_type_is(operation_canceled_exception, Expected) -> + exception_type_is(system_exception, Expected); +exception_type_is(Type, Expected) + when Type =:= format_exception; + Type =:= index_out_of_range_exception; + Type =:= invalid_operation_exception; + Type =:= not_implemented_exception; + Type =:= not_supported_exception; + Type =:= null_reference_exception; + Type =:= out_of_memory_exception; + Type =:= rank_exception; + Type =:= stack_overflow_exception; + Type =:= timeout_exception -> + exception_type_is(system_exception, Expected); +exception_type_is(system_exception, Expected) -> exception_type_is(exception, Expected); +exception_type_is(_, _) -> false. + exception_message(#{message := Message}) -> Message; exception_message(Exception) when is_reference(Exception) -> exception_message(get(Exception)); exception_message(Exception) -> format_term(Exception). diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index 4168846f65..10e63de361 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -734,6 +734,21 @@ let ``test try-with with unmatched exception type reraises`` () = | _ -> caught <- "other" caught |> equal "arg" +[] +let ``test built-in exception type tests respect nominal hierarchy`` () = + let invalidOperation: exn = System.InvalidOperationException("invalid") + let argumentNull: exn = System.ArgumentNullException("value") + let divideByZero: exn = System.DivideByZeroException() + let aggregate: exn = System.AggregateException() + + (invalidOperation :? System.InvalidOperationException) |> equal true + (invalidOperation :? System.ArgumentException) |> equal false + (box invalidOperation :? System.Exception) |> equal true + (argumentNull :? System.ArgumentException) |> equal true + (divideByZero :? System.ArithmeticException) |> equal true + (aggregate :? System.AggregateException) |> equal true + (aggregate :? System.ArgumentException) |> equal false + [] let ``test ArgumentException with message and inner exception works`` () = let inner = exn "the inner cause" diff --git a/tests/Beam/SeqExpressionTests.fs b/tests/Beam/SeqExpressionTests.fs index 8562a99984..5b44092b9e 100644 --- a/tests/Beam/SeqExpressionTests.fs +++ b/tests/Beam/SeqExpressionTests.fs @@ -111,25 +111,23 @@ let ``test try...with in seq expressions preserves yielded elements before the e equal [1; 3] xs equal true caught -// TODO: enable when Beam correctly rethrows exceptions not matched by a filtered -// try/with handler in seq expressions (see PR #4719 review discussion) -// [] -// let ``test try...with in seq expressions rethrows unmatched exceptions`` () = -// let mutable propagated = false -// try -// seq { -// try -// yield 1 -// raise (System.InvalidOperationException "boom") -// yield 2 -// with :? System.ArgumentException -> -// yield 0 -// } -// |> Seq.toList -// |> ignore -// with :? System.InvalidOperationException -> -// propagated <- true -// equal true propagated +[] +let ``test try...with in seq expressions rethrows unmatched exceptions`` () = + let mutable propagated = false + try + seq { + try + yield 1 + raise (System.InvalidOperationException "boom") + yield 2 + with :? System.ArgumentException -> + yield 0 + } + |> Seq.toList + |> ignore + with :? System.InvalidOperationException -> + propagated <- true + equal true propagated [] let ``test array expressions work`` () = diff --git a/tests/Beam/TryCatchTests.fs b/tests/Beam/TryCatchTests.fs index 2d6ab7d4b7..388023d10b 100644 --- a/tests/Beam/TryCatchTests.fs +++ b/tests/Beam/TryCatchTests.fs @@ -3,6 +3,16 @@ module Fable.Tests.TryCatch open Fable.Tests.Util open Util.Testing +#if FABLE_COMPILER_BEAM +open Fable.Core + +[] +let private throwNative () : unit = nativeOnly + +[ true; _:_ -> false end")>] +let private catchesNativeThrow (f: unit -> unit) : bool = nativeOnly +#endif + [] let ``test try-catch returns body value on success`` () = let result = @@ -96,6 +106,18 @@ let ``test reraise in try-catch with side effect`` () = sideEffectRan |> equal true msg |> equal "boom" +#if FABLE_COMPILER_BEAM +[] +let ``test reraise preserves native Erlang exception class`` () = + let rethrow () = + try + throwNative () + with _ -> + reraise () + + catchesNativeThrow rethrow |> equal true +#endif + [] let ``test try-catch result used in match`` () = // Regression: try/catch result fed into pattern match (the Seq.fs generateWhileSome pattern) From dc964762b81c0f50de23113bf671050f544037df Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 20:21:36 +0200 Subject: [PATCH 102/322] fix(beam): support integer Log2 (#5002) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 2 +- src/Fable.Transforms/Beam/Replacements.fs | 10 ++++++-- src/fable-library-beam/fable_int.erl | 27 +++++++++++++++++++- tests/Beam/ArithmeticTests.fs | 31 ++++++++++++++++------- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 19e8db2d7f..f5b032bbcf 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -464,7 +464,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | Some byref `TryParse`/`DivRem` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, integer `Log2`, and special floating-point values need parity work. | +| Numeric APIs | Some byref `TryParse`/`DivRem` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index f93e9cfcbf..504d837be9 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -326,7 +326,13 @@ let private operators | _ -> emitExpr r _t [ arg ] "float(erlang:floor($0))" |> Some | "Log", [ arg ] -> emitExpr r _t [ arg ] "math:log($0)" |> Some | "Log10", [ arg ] -> emitExpr r _t [ arg ] "math:log10($0)" |> Some - | "Log2", [ arg ] -> emitExpr r _t [ arg ] "math:log2($0)" |> Some + | "Log2", [ arg ] -> + match arg.Type with + | Type.Number(Float16, _) + | Type.Number(Float32, _) + | Type.Number(Float64, _) -> emitExpr r _t [ arg ] "math:log2($0)" |> Some + | Type.Number _ -> Helper.LibCall(com, "fable_int", "log2", _t, [ arg ], ?loc = r) |> Some + | _ -> emitExpr r _t [ arg ] "math:log2($0)" |> Some | ("Pow" | "PowInteger" | "op_Exponentiation"), [ base_; exp_ ] -> match base_.Type with | Type.Number(Decimal, _) -> @@ -5719,7 +5725,7 @@ let tryCall | "Log", None, [ arg ] -> emitExpr r t [ arg ] "math:log(float($0))" |> Some | "Log", None, [ arg; base_ ] -> emitExpr r t [ arg; base_ ] "(math:log(float($0)) / math:log($1))" |> Some | "Log10", None, [ arg ] -> emitExpr r t [ arg ] "math:log10(float($0))" |> Some - | "Log2", None, [ arg ] -> emitExpr r t [ arg ] "math:log2(float($0))" |> Some + | "Log2", None, [ arg ] -> Helper.LibCall(com, "fable_int", "log2", t, [ arg ], ?loc = r) |> Some | "DivRem", None, [ x; y ] -> emitExpr r t [ x; y ] "{$0 div $1, $0 rem $1}" |> Some | "DivRem", None, [ x; y; refRem ] -> Helper.LibCall(com, "fable_utils", "div_rem", t, [ x; y; refRem ], ?loc = r) diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 0cbbd5d04c..8e71213c08 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -7,7 +7,8 @@ wrap_u8/1, wrap_u16/1, wrap_u32/1, - wrap_u64/1 + wrap_u64/1, + log2/1 ]). -spec wrap_i8(integer()) -> integer(). @@ -18,6 +19,7 @@ -spec wrap_u16(integer()) -> non_neg_integer(). -spec wrap_u32(integer()) -> non_neg_integer(). -spec wrap_u64(integer()) -> non_neg_integer(). +-spec log2(integer()) -> non_neg_integer(). %% Fixed-width (two's complement) integer semantics for .NET sized integers. %% @@ -71,3 +73,26 @@ wrap_u64(N) when N >= 0, N =< 16#FFFFFFFFFFFFFFFF -> N; wrap_u64(N) -> <> = <>, V. + +%% Compute integer log2 from bit length rather than floating point so UInt64 and +%% arbitrary-precision values stay exact above the IEEE-754 integer precision limit. +log2(N) when N < 0 -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"Non-negative number required. (Parameter 'value')">> + }); +log2(0) -> + 0; +log2(N) -> + Bin = binary:encode_unsigned(N), + <> = Bin, + (byte_size(Bin) - 1) * 8 + log2_byte(MostSignificantByte). + +log2_byte(N) when N >= 16#80 -> 7; +log2_byte(N) when N >= 16#40 -> 6; +log2_byte(N) when N >= 16#20 -> 5; +log2_byte(N) when N >= 16#10 -> 4; +log2_byte(N) when N >= 16#08 -> 3; +log2_byte(N) when N >= 16#04 -> 2; +log2_byte(N) when N >= 16#02 -> 1; +log2_byte(_) -> 0. diff --git a/tests/Beam/ArithmeticTests.fs b/tests/Beam/ArithmeticTests.fs index 647354b1b3..5746bfdf4e 100644 --- a/tests/Beam/ArithmeticTests.fs +++ b/tests/Beam/ArithmeticTests.fs @@ -940,15 +940,28 @@ let ``test Numeric Clamp works`` () = Decimal.Clamp(5.0M, -4.0M, 3.0M) |> equal 3.0M bigint.Clamp(5I, -4I, 3I) |> equal 3I -// TODO: Erlang math:log2 returns float, but Numeric Log2 for integer types expects integer result. -// SByte.Log2, Int16.Log2, Byte.Log2, UInt16.Log2, UInt32.Log2, UInt64.Log2 would need trunc(). -// [] -// let ``test Numeric Log2 works`` () = -// Int32.Log2 8 |> equal 3 -// Int64.Log2 8L |> equal 3L -// Double.Log2 8.0 |> equal 3.0 -// Single.Log2 8.0f |> equal 3.0f -// bigint.Log2 8I |> equal 3I +[] +let ``test Numeric Log2 works`` () = + SByte.Log2 8y |> equal 3y + Int16.Log2 8s |> equal 3s + Int32.Log2 8 |> equal 3 + Int64.Log2 8L |> equal 3L + Byte.Log2 8uy |> equal 3uy + UInt16.Log2 8us |> equal 3us + UInt32.Log2 8u |> equal 3u + UInt64.Log2 8UL |> equal 3UL + Double.Log2 8.0 |> equal 3.0 + Single.Log2 8.0f |> equal 3.0f + bigint.Log2 8I |> equal 3I + + Int32.Log2 0 |> equal 0 + Int32.Log2 5 |> equal 2 + Int64.Log2 Int64.MaxValue |> equal 62L + UInt64.Log2 UInt64.MaxValue |> equal 63UL + bigint.Log2 ((1I <<< 256) + 1I) |> equal 256I + + throwsAnyError (fun () -> Int64.Log2 -1L |> ignore) + throwsAnyError (fun () -> bigint.Log2 -1I |> ignore) [] let ``test Math.Clamp expanded works`` () = From 902870b15f46a90ecd034fb40d97e16de4a10713 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 20:44:33 +0200 Subject: [PATCH 103/322] fix(beam): support DivRem ref arguments (#5003) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 2 +- src/Fable.Transforms/Beam/Replacements.fs | 7 ++++- tests/Beam/ArithmeticTests.fs | 32 ++++++++++------------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index f5b032bbcf..bc67330b99 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -464,7 +464,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | Some byref `TryParse`/`DivRem` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | +| Numeric APIs | Some byref `TryParse` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 504d837be9..d17885b4a5 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -3097,7 +3097,12 @@ let makeRefFromMutableField (callee: Expr) (key: string) = - Get(callee, FieldInfo.Create(key, isMutable = true), t, r) + match callee.Type, key with + | DeclaredType(ent, _), "contents" when ent.FullName = Types.refCell -> + // A BEAM FSharpRef is itself a process-dictionary key, so its contents + // address is the ref expression rather than a field of a record/map. + callee + | _ -> Get(callee, FieldInfo.Create(key, isMutable = true), t, r) let asyncBuilder (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (_thisArg: Expr option) (args: Expr list) = match i.CompiledName with diff --git a/tests/Beam/ArithmeticTests.fs b/tests/Beam/ArithmeticTests.fs index 5746bfdf4e..26cc30e78d 100644 --- a/tests/Beam/ArithmeticTests.fs +++ b/tests/Beam/ArithmeticTests.fs @@ -836,30 +836,26 @@ let ``test Math.DivRem works with ints`` () = Math.DivRem(5, 2) |> equal (2, 1) Math.DivRem(4, 2) |> equal (2, 0) -// TODO: DivRem with ref requires byref/address-of support which uses different -// ref patterns in Beam (process dictionary refs vs map-based refs). -// [] -// let ``test Math.DivRem works with ints and ref`` () = -// let rem = ref -1 -// Math.DivRem(5, 2, rem) |> equal 2 -// rem.Value |> equal 1 -// Math.DivRem(4, 2, rem) |> equal 2 -// rem.Value |> equal 0 +[] +let ``test Math.DivRem works with ints and ref`` () = + let rem = ref -1 + Math.DivRem(5, 2, rem) |> equal 2 + rem.Value |> equal 1 + Math.DivRem(4, 2, rem) |> equal 2 + rem.Value |> equal 0 [] let ``test Math.DivRem works with longs`` () = Math.DivRem(5L, 2L) |> equal (2L, 1L) Math.DivRem(4L, 2L) |> equal (2L, 0L) -// TODO: DivRem with ref requires byref/address-of support which uses different -// ref patterns in Beam (process dictionary refs vs map-based refs). -// [] -// let ``test Math.DivRem works with longs and ref`` () = -// let rem = ref -1L -// Math.DivRem(5L, 2L, rem) |> equal 2L -// rem.Value |> equal 1L -// Math.DivRem(4L, 2L, rem) |> equal 2L -// rem.Value |> equal 0L +[] +let ``test Math.DivRem works with longs and ref`` () = + let rem = ref -1L + Math.DivRem(5L, 2L, rem) |> equal 2L + rem.Value |> equal 1L + Math.DivRem(4L, 2L, rem) |> equal 2L + rem.Value |> equal 0L // --- Math.MinMagnitude / MaxMagnitude --- From 4ab553a414d01140afd252b350912fd8a900f080 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 21:03:53 +0200 Subject: [PATCH 104/322] test(beam): restore TryParse parity coverage (#5004) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 4 +- tests/Beam/ConversionTests.fs | 60 ++++++++++++------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index bc67330b99..3b5ed63e16 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -464,7 +464,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | Some byref `TryParse` paths, range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | +| Numeric APIs | Range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | @@ -501,7 +501,7 @@ visible rather than silent correctness failures. | P0 | Option erasure loses states in generic and null-like paths | Carry the nested-option decision through replacements and collection helpers, or adopt an unambiguous tagged form where erasure is unsafe. | | P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | | P0 | Object-model gaps affect valid F# | Fix silent wrong-code paths in the claimed object-model surface. Keep unsupported class and struct forms as explicit exclusions until implemented. | -| P0 | Numeric and byref APIs have correctness gaps | Fix incorrect results in claimed numeric APIs. Missing APIs can remain documented exclusions; restore regression tests as implementations land. | +| P0 | Numeric APIs have correctness gaps | Fix incorrect results in claimed numeric APIs. Missing APIs can remain documented exclusions; restore regression tests as implementations land. | ### Fidelity and diagnostics diff --git a/tests/Beam/ConversionTests.fs b/tests/Beam/ConversionTests.fs index 23c14a0c0c..349afcbb7d 100644 --- a/tests/Beam/ConversionTests.fs +++ b/tests/Beam/ConversionTests.fs @@ -572,23 +572,20 @@ let ``test Convert.ToBase64String works`` () = // --- TryParse methods --- -// TODO: TryParse via higher-order function — auto-generated lambda unwraps ref.contents -// instead of passing the ref directly to try_parse_float (which needs it for put/2). -// Fixing requires changes to how byref method references are compiled. -// [] -// let ``test System.Double.TryParse works`` () = -// tryParse Double.TryParse 0.0 "1" |> equal (true, 1.0) -// tryParse Double.TryParse 0.0 "1.5" |> equal (true, 1.5) -// tryParse Double.TryParse 0.0 "foo" |> equal (false, 0.0) -// tryParse Double.TryParse 0.0 "" |> equal (false, 0.0) -// tryParse Double.TryParse 0.0 "-1.5" |> equal (true, -1.5) -// -// [] -// let ``test System.Single.TryParse works`` () = -// tryParse Single.TryParse 0.0f "1" |> equal (true, 1.0f) -// tryParse Single.TryParse 0.0f "1.5" |> equal (true, 1.5f) -// tryParse Single.TryParse 0.0f "foo" |> equal (false, 0.0f) -// tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) +[] +let ``test System.Double.TryParse works`` () = + tryParse Double.TryParse 0.0 "1" |> equal (true, 1.0) + tryParse Double.TryParse 0.0 "1.5" |> equal (true, 1.5) + tryParse Double.TryParse 0.0 "foo" |> equal (false, 0.0) + tryParse Double.TryParse 0.0 "" |> equal (false, 0.0) + tryParse Double.TryParse 0.0 "-1.5" |> equal (true, -1.5) + +[] +let ``test System.Single.TryParse works`` () = + tryParse Single.TryParse 0.0f "1" |> equal (true, 1.0f) + tryParse Single.TryParse 0.0f "1.5" |> equal (true, 1.5f) + tryParse Single.TryParse 0.0f "foo" |> equal (false, 0.0f) + tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) [] let ``test System.Boolean.TryParse works`` () = @@ -599,21 +596,20 @@ let ``test System.Boolean.TryParse works`` () = Boolean.TryParse "tru" |> equal (false, false) Boolean.TryParse "falsee" |> equal (false, false) -// TODO: TryParse via higher-order function — same byref method reference issue as above -// [] -// let ``test System.Int64.TryParse works`` () = -// tryParse Int64.TryParse 0L "99" |> equal (true, 99L) -// tryParse Int64.TryParse 0L "foo" |> equal (false, 0L) -// -// [] -// let ``test System.UInt32.TryParse works`` () = -// tryParse UInt32.TryParse 0u "99" |> equal (true, 99u) -// tryParse UInt32.TryParse 0u "foo" |> equal (false, 0u) -// -// [] -// let ``test System.UInt64.TryParse works`` () = -// tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) -// tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) +[] +let ``test System.Int64.TryParse works`` () = + tryParse Int64.TryParse 0L "99" |> equal (true, 99L) + tryParse Int64.TryParse 0L "foo" |> equal (false, 0L) + +[] +let ``test System.UInt32.TryParse works`` () = + tryParse UInt32.TryParse 0u "99" |> equal (true, 99u) + tryParse UInt32.TryParse 0u "foo" |> equal (false, 0u) + +[] +let ``test System.UInt64.TryParse works`` () = + tryParse UInt64.TryParse 0UL "99" |> equal (true, 99UL) + tryParse UInt64.TryParse 0UL "foo" |> equal (false, 0UL) // --- Single.Parse --- From d56963043d6f24296d58fae34b4288cc950315ff Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 21:25:47 +0200 Subject: [PATCH 105/322] fix(beam): range-check integer parsing (#5005) --- src/Fable.Transforms/Beam/Replacements.fs | 57 ++++++++++++++----- src/fable-library-beam/fable_convert.erl | 67 +++++++++++++++++++++++ tests/Beam/ConversionTests.fs | 57 +++++++++++++++++++ 3 files changed, 166 insertions(+), 15 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index d17885b4a5..79dcccb02d 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -23,6 +23,32 @@ let private wrapToIntType (com: ICompiler) r (t: Type) (sourceType: Type) (expr: Helper.LibCall(com, "fable_int", Integers.wrapFunctionName info, t, [ expr ], ?loc = r) | _ -> expr +let private integerRangeArgs (bits, signed) = + [ makeIntConst bits; makeBoolConst signed ] + +let private sizedIntInfoForEntity = + function + | Types.int8 -> Some(8, true) + | Types.uint8 -> Some(8, false) + | Types.int16 -> Some(16, true) + | Types.uint16 -> Some(16, false) + | Types.int32 -> Some(32, true) + | Types.uint32 -> Some(32, false) + | Types.int64 -> Some(64, true) + | Types.uint64 -> Some(64, false) + | _ -> None + +let private checkedIntegerCall (com: ICompiler) r t funcName args rangeInfo = + let args = + match rangeInfo with + | Some info -> args @ integerRangeArgs info + | None -> args + + Helper.LibCall(com, "fable_convert", funcName, t, args, ?loc = r) + +let private checkedIntegerCallForType com r t funcName args = + checkedIntegerCall com r t funcName args (Integers.sizedIntInfo t) + /// Convert an expression *into* a BigInteger. Erlang integers are already arbitrary-precision, so /// this is the identity for an integral source. A float must be truncated toward zero, the way /// `BigInteger(double)` does: leaving it as a float would hand a non-integer to the integer @@ -405,9 +431,7 @@ let private operators | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), [ arg ] -> match arg.Type with - // TODO: `fable_convert:to_int` does not range-check against the target type, so - // out-of-range input yields the parsed value instead of raising the way .NET does. - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", _t, [ arg ], ?loc = r) |> Some + | Type.String -> checkedIntegerCallForType com r _t "to_int" [ arg ] |> Some | Type.Number(kind, _) -> match kind with | Decimal -> @@ -723,10 +747,8 @@ let private languagePrimitives | "DivideByInt", [ left; right ] -> makeBinOp r t left right BinaryDivide |> Some // IntrinsicFunctions within LanguagePrimitives | "UnboxFast", [ arg ] -> TypeCast(arg, t) |> Some - | ("ParseInt32" | "ParseUInt32"), [ arg ] -> - Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some - | ("ParseInt64" | "ParseUInt64"), [ arg ] -> - Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some + | ("ParseInt32" | "ParseUInt32"), [ arg ] -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some + | ("ParseInt64" | "ParseUInt64"), [ arg ] -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | _ -> None let private unchecked @@ -1394,7 +1416,7 @@ let private conversions | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), [ arg ] -> match arg.Type with - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some + | Type.String -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | Type.Number(kind, _) -> match kind with | Float16 @@ -1459,10 +1481,11 @@ let private numericTypes | "System.Decimal" -> Helper.LibCall(com, "fable_decimal", "parse", t, [ arg ], ?loc = r) |> Some | _ -> // Int32, Int64, Byte, etc. — all parse to integer - emitExpr r t [ arg ] "binary_to_integer($0)" |> Some + checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | "Parse", None, [ arg; _style ] -> // NumberStyles.HexNumber — parse hex string to integer - emitExpr r t [ arg ] "binary_to_integer($0, 16)" |> Some + checkedIntegerCallForType com r t "to_int_with_base" [ arg; makeIntConst 16 ] + |> Some | "ToString", Some c, [] -> match c.Type with | Type.Number(kind, _) -> @@ -1505,7 +1528,13 @@ let private numericTypes Helper.LibCall(com, "fable_decimal", "try_parse", t, [ str; outRef ], ?loc = r) |> Some | _ -> - Helper.LibCall(com, "fable_convert", "try_parse_int", t, [ str; outRef ], ?loc = r) + checkedIntegerCall + com + r + t + "try_parse_int" + [ str; outRef ] + (sizedIntInfoForEntity info.DeclaringEntityFullName) |> Some // Numeric type static methods that delegate to operators | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Log2" | "DivRem"), _, _ -> @@ -1564,7 +1593,7 @@ let private convert = let toInt (arg: Expr) = match arg.Type with - | Type.String -> Helper.LibCall(com, "fable_convert", "to_int", t, [ arg ], ?loc = r) |> Some + | Type.String -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some | Type.Char -> Some arg | Type.Number(kind, _) -> match kind with @@ -1591,9 +1620,7 @@ let private convert | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), [ arg ] -> toInt arg | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), - [ arg; baseArg ] -> - Helper.LibCall(com, "fable_convert", "to_int_with_base", t, [ arg; baseArg ], ?loc = r) - |> Some + [ arg; baseArg ] -> checkedIntegerCallForType com r t "to_int_with_base" [ arg; baseArg ] |> Some | ("ToSingle" | "ToDouble"), [ arg ] -> toFloat arg | "ToChar", [ arg ] -> match arg.Type with diff --git a/src/fable-library-beam/fable_convert.erl b/src/fable-library-beam/fable_convert.erl index f29abda335..1f1b550dee 100644 --- a/src/fable-library-beam/fable_convert.erl +++ b/src/fable-library-beam/fable_convert.erl @@ -2,7 +2,9 @@ -export([ to_float/1, to_int/1, + to_int/3, to_int_with_base/2, + to_int_with_base/4, to_string/1, to_string_with_base/3, to_base64/1, @@ -11,12 +13,15 @@ boolean_try_parse/2, int_to_string_with_format/2, try_parse_int/2, + try_parse_int/4, try_parse_float/2 ]). -spec to_float(binary() | integer() | float()) -> float(). -spec to_int(binary() | integer() | float()) -> integer(). +-spec to_int(binary() | integer() | float(), pos_integer(), boolean()) -> integer(). -spec to_int_with_base(binary(), integer()) -> integer(). +-spec to_int_with_base(binary(), integer(), pos_integer(), boolean()) -> integer(). -spec to_string(term()) -> binary(). -spec to_string_with_base(integer(), integer(), integer()) -> binary(). -spec to_base64(tuple() | list() | binary()) -> binary(). @@ -25,6 +30,7 @@ -spec boolean_try_parse(binary(), reference()) -> boolean(). -spec int_to_string_with_format(integer(), binary()) -> binary(). -spec try_parse_int(binary(), reference()) -> boolean(). +-spec try_parse_int(binary(), reference(), pos_integer(), boolean()) -> boolean(). -spec try_parse_float(binary(), reference()) -> boolean(). %% Robust string-to-float conversion that handles edge cases @@ -68,10 +74,54 @@ to_int(Bin) when is_binary(Bin) -> to_int(N) when is_integer(N) -> N; to_int(F) when is_float(F) -> trunc(F). +to_int(Bin, Bits, Signed) when is_binary(Bin) -> + case Bin of + <<"0x", Rest/binary>> -> to_int_with_base(Rest, 16, Bits, Signed); + <<"0X", Rest/binary>> -> to_int_with_base(Rest, 16, Bits, Signed); + <<"0o", Rest/binary>> -> to_int_with_base(Rest, 8, Bits, Signed); + <<"0O", Rest/binary>> -> to_int_with_base(Rest, 8, Bits, Signed); + <<"0b", Rest/binary>> -> to_int_with_base(Rest, 2, Bits, Signed); + <<"0B", Rest/binary>> -> to_int_with_base(Rest, 2, Bits, Signed); + _ -> ensure_integer_range(binary_to_integer(Bin), Bits, Signed) + end; +to_int(N, Bits, Signed) when is_integer(N) -> ensure_integer_range(N, Bits, Signed); +to_int(F, Bits, Signed) when is_float(F) -> ensure_integer_range(trunc(F), Bits, Signed). + %% Parse string to integer with given base (2, 8, 10, 16) to_int_with_base(Bin, Base) when is_binary(Bin), is_integer(Base) -> binary_to_integer(Bin, Base). +to_int_with_base(Bin, Base, Bits, Signed) when is_binary(Bin), is_integer(Base) -> + checked_base_integer(binary_to_integer(Bin, Base), Base, Bits, Signed). + +%% Non-decimal signed parsing accepts the target width's two's-complement form. +%% invariant: checked integer parsing never returns a value outside the selected target width. +checked_base_integer(N, Base, Bits, true) when Base =/= 10 -> + SignBit = 1 bsl (Bits - 1), + Modulus = 1 bsl Bits, + case N >= SignBit andalso N < Modulus of + true -> N - Modulus; + false -> ensure_integer_range(N, Bits, true) + end; +checked_base_integer(N, _Base, Bits, Signed) -> + ensure_integer_range(N, Bits, Signed). + +ensure_integer_range(N, Bits, Signed) -> + case integer_in_range(N, Bits, Signed) of + true -> N; + false -> + erlang:error(#{ + exn_type => overflow_exception, + message => <<"Value was either too large or too small for an integer type.">> + }) + end. + +integer_in_range(N, Bits, true) -> + Limit = 1 bsl (Bits - 1), + N >= -Limit andalso N < Limit; +integer_in_range(N, Bits, false) -> + N >= 0 andalso N < (1 bsl Bits). + %% Convert integer to string with given base and bit width %% BitWidth: 8 (SByte), 16 (Int16), 32 (Int32), 64 (Int64) %% For negative numbers with non-decimal bases, .NET uses two's complement @@ -132,6 +182,23 @@ try_parse_int(Bin, OutRef) when is_binary(Bin) -> try_parse_int(_, _) -> false. +try_parse_int(Bin, OutRef, Bits, Signed) when is_binary(Bin) -> + Trimmed = string:trim(binary_to_list(Bin)), + case string:to_integer(Trimmed) of + {Int, []} -> + case integer_in_range(Int, Bits, Signed) of + true -> + put(OutRef, Int), + true; + false -> + false + end; + _ -> + false + end; +try_parse_int(_, _, _, _) -> + false. + try_parse_float(Bin, OutRef) when is_binary(Bin) -> Trimmed = string:trim(binary_to_list(Bin)), case string:to_float(Trimmed) of diff --git a/tests/Beam/ConversionTests.fs b/tests/Beam/ConversionTests.fs index 349afcbb7d..5936370c55 100644 --- a/tests/Beam/ConversionTests.fs +++ b/tests/Beam/ConversionTests.fs @@ -20,6 +20,14 @@ let parse f (a: string) = f(a, System.Globalization.CultureInfo("en-US")) #endif +let throwsOverflow (f: unit -> unit) = + try + f () + false + with + | :? OverflowException -> true + | _ -> false + // --- Basic type conversions --- [] @@ -34,6 +42,14 @@ let ``test negative int to string works`` () = let ``test string to int works`` () = int "42" |> equal 42 +[] +let ``test string to integer conversion checks the target range`` () = + int "2147483647" |> equal Int32.MaxValue + byte "255" |> equal Byte.MaxValue + (fun () -> int "2147483648" |> ignore) |> throwsOverflow |> equal true + (fun () -> byte "256" |> ignore) |> throwsOverflow |> equal true + (fun () -> uint32 "-1" |> ignore) |> throwsOverflow |> equal true + [] let ``test float to int works`` () = int 3.14 |> equal 3 @@ -301,11 +317,19 @@ let ``test System.Convert.ToChar works`` () = let ``test System.Int32.Parse works`` () = Int32.Parse("5") |> equal 5 Int32.Parse("-5") |> equal -5 + Int32.Parse("-2147483648") |> equal Int32.MinValue + Int32.Parse("2147483647") |> equal Int32.MaxValue + (fun () -> Int32.Parse("2147483648") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int32.Parse("-2147483649") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Int64.Parse works`` () = Int64.Parse("5") |> equal 5L Int64.Parse("-5") |> equal -5L + Int64.Parse("-9223372036854775808") |> equal Int64.MinValue + Int64.Parse("9223372036854775807") |> equal Int64.MaxValue + (fun () -> Int64.Parse("9223372036854775808") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int64.Parse("-9223372036854775809") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Double.Parse works`` () = @@ -411,31 +435,46 @@ let ``test System.SByte.Parse works`` () = SByte.Parse("5") |> equal 5y SByte.Parse("-5") |> equal -5y SByte.Parse("-128") |> equal -128y + SByte.Parse("127") |> equal SByte.MaxValue + (fun () -> SByte.Parse("128") |> ignore) |> throwsOverflow |> equal true + (fun () -> SByte.Parse("-129") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Int16.Parse works`` () = Int16.Parse("5") |> equal 5s Int16.Parse("-5") |> equal -5s Int16.Parse("-32768") |> equal -32768s + Int16.Parse("32767") |> equal Int16.MaxValue + (fun () -> Int16.Parse("32768") |> ignore) |> throwsOverflow |> equal true + (fun () -> Int16.Parse("-32769") |> ignore) |> throwsOverflow |> equal true [] let ``test System.Byte.Parse works`` () = Byte.Parse("5") |> equal 5uy Byte.Parse("255") |> equal 255uy + (fun () -> Byte.Parse("256") |> ignore) |> throwsOverflow |> equal true + (fun () -> Byte.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt16.Parse works`` () = UInt16.Parse("5") |> equal 5us UInt16.Parse("65535") |> equal 65535us + (fun () -> UInt16.Parse("65536") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt16.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt32.Parse works`` () = UInt32.Parse("5") |> equal 5u UInt32.Parse("4294967295") |> equal 4294967295u + (fun () -> UInt32.Parse("4294967296") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt32.Parse("-1") |> ignore) |> throwsOverflow |> equal true [] let ``test System.UInt64.Parse works`` () = UInt64.Parse("5") |> equal 5uL + UInt64.Parse("18446744073709551615") |> equal UInt64.MaxValue + (fun () -> UInt64.Parse("18446744073709551616") |> ignore) |> throwsOverflow |> equal true + (fun () -> UInt64.Parse("-1") |> ignore) |> throwsOverflow |> equal true // --- Convert with base parameter --- @@ -464,6 +503,7 @@ let ``test System.Convert.ToByte with base works`` () = Convert.ToByte(x, 2) |> equal 5uy Convert.ToByte(x, 8) |> equal 65uy Convert.ToByte(x, 10) |> equal 101uy + (fun () -> Convert.ToByte("100", 16) |> ignore) |> throwsOverflow |> equal true [] let ``test System.Convert.ToInt16 with base works`` () = @@ -473,6 +513,7 @@ let ``test System.Convert.ToInt16 with base works`` () = Convert.ToInt16(x, 8) |> equal 65s Convert.ToInt16(x, 10) |> equal 101s Convert.ToInt16(x, 16) |> equal 257s + Convert.ToInt16("FFFF", 16) |> equal -1s // --- Convert.ToString with base parameter --- @@ -556,9 +597,25 @@ let ``test System.Int32.TryParse works`` () = success2 |> equal false value2 |> equal 0 +[] +let ``test integer TryParse checks the target range`` () = + SByte.TryParse("128") |> equal (false, 0y) + Byte.TryParse("-1") |> equal (false, 0uy) + Int16.TryParse("32768") |> equal (false, 0s) + UInt16.TryParse("65536") |> equal (false, 0us) + Int32.TryParse("2147483648") |> equal (false, 0) + UInt32.TryParse("4294967296") |> equal (false, 0u) + Int64.TryParse("9223372036854775808") |> equal (false, 0L) + UInt64.TryParse("18446744073709551616") |> equal (false, 0uL) + [] let ``test System.Int32.Parse with hex works`` () = Int32.Parse("5f", System.Globalization.NumberStyles.HexNumber) |> equal 95 + Int32.Parse("FFFFFFFF", System.Globalization.NumberStyles.HexNumber) |> equal -1 + + (fun () -> Int32.Parse("1FFFFFFFF", System.Globalization.NumberStyles.HexNumber) |> ignore) + |> throwsOverflow + |> equal true [] let ``test BitConverter.GetBytes Int32 works`` () = From 3ebe480256605e244795e9aa088bbee03347689d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 21:55:14 +0200 Subject: [PATCH 106/322] fix(beam): check System.Convert integer conversions (#5006) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 2 +- src/Fable.Transforms/Beam/Replacements.fs | 21 ++++----- src/fable-library-beam/fable_convert.erl | 28 ++++++++++++ tests/Beam/ConversionTests.fs | 52 +++++++++++++++++++++++ 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 3b5ed63e16..d610fa5339 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -464,7 +464,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | Range-checked conversion, decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | +| Numeric APIs | Decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 79dcccb02d..e2edea5ee3 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -1594,15 +1594,16 @@ let private convert let toInt (arg: Expr) = match arg.Type with | Type.String -> checkedIntegerCallForType com r t "to_int" [ arg ] |> Some - | Type.Char -> Some arg - | Type.Number(kind, _) -> - match kind with - | Float16 - | Float32 - | Float64 - | Decimal -> emitExpr r t [ arg ] "trunc($0)" |> Some - | _ -> Some arg - | _ -> Some arg + | Type.Number(Decimal, _) -> + let rounded = + Helper.LibCall(com, "fable_decimal", "round_decimal", arg.Type, [ arg ], ?loc = r) + + let integerValue = + Helper.LibCall(com, "fable_decimal", "to_int", t, [ rounded ], ?loc = r) + + checkedIntegerCallForType com r t "convert_to_int" [ integerValue ] |> Some + // decision: System.Convert is checked; F# casts keep their separate wrap/truncate path. + | _ -> checkedIntegerCallForType com r t "convert_to_int" [ arg ] |> Some let toFloat (arg: Expr) = match arg.Type with @@ -1625,7 +1626,7 @@ let private convert | "ToChar", [ arg ] -> match arg.Type with | Type.String -> emitExpr r t [ arg ] "binary:first($0)" |> Some - | _ -> Some arg + | _ -> toInt arg | "ToString", [ arg ] -> match arg.Type with | Type.String -> Some arg diff --git a/src/fable-library-beam/fable_convert.erl b/src/fable-library-beam/fable_convert.erl index 1f1b550dee..8e5dd5627e 100644 --- a/src/fable-library-beam/fable_convert.erl +++ b/src/fable-library-beam/fable_convert.erl @@ -3,6 +3,7 @@ to_float/1, to_int/1, to_int/3, + convert_to_int/3, to_int_with_base/2, to_int_with_base/4, to_string/1, @@ -20,6 +21,7 @@ -spec to_float(binary() | integer() | float()) -> float(). -spec to_int(binary() | integer() | float()) -> integer(). -spec to_int(binary() | integer() | float(), pos_integer(), boolean()) -> integer(). +-spec convert_to_int(boolean() | integer() | float(), pos_integer(), boolean()) -> integer(). -spec to_int_with_base(binary(), integer()) -> integer(). -spec to_int_with_base(binary(), integer(), pos_integer(), boolean()) -> integer(). -spec to_string(term()) -> binary(). @@ -87,6 +89,32 @@ to_int(Bin, Bits, Signed) when is_binary(Bin) -> to_int(N, Bits, Signed) when is_integer(N) -> ensure_integer_range(N, Bits, Signed); to_int(F, Bits, Signed) when is_float(F) -> ensure_integer_range(trunc(F), Bits, Signed). +%% System.Convert rounds floating-point values before checking the target range. +%% invariant: checked conversions round midpoint values to even and stay within the target width. +convert_to_int(true, Bits, Signed) -> ensure_integer_range(1, Bits, Signed); +convert_to_int(false, Bits, Signed) -> ensure_integer_range(0, Bits, Signed); +convert_to_int(N, Bits, Signed) when is_integer(N) -> ensure_integer_range(N, Bits, Signed); +convert_to_int(F, Bits, Signed) when is_float(F) -> + ensure_integer_range(round_half_to_even(F), Bits, Signed). + +round_half_to_even(F) -> + Truncated = trunc(F), + Fraction = F - Truncated, + AbsFraction = abs(Fraction), + if + AbsFraction < 0.5 -> + Truncated; + AbsFraction > 0.5 -> + Truncated + round_direction(Fraction); + Truncated rem 2 =:= 0 -> + Truncated; + true -> + Truncated + round_direction(Fraction) + end. + +round_direction(Fraction) when Fraction < 0 -> -1; +round_direction(_Fraction) -> 1. + %% Parse string to integer with given base (2, 8, 10, 16) to_int_with_base(Bin, Base) when is_binary(Bin), is_integer(Base) -> binary_to_integer(Bin, Base). diff --git a/tests/Beam/ConversionTests.fs b/tests/Beam/ConversionTests.fs index 5936370c55..cf08ef23e2 100644 --- a/tests/Beam/ConversionTests.fs +++ b/tests/Beam/ConversionTests.fs @@ -299,6 +299,58 @@ let ``test System.Convert.ToUInt64 works`` () = Convert.ToUInt64(1.) |> equal x Convert.ToUInt64("1") |> equal x +[] +let ``test System.Convert integer conversions round midpoint values to even`` () = + Convert.ToInt32(1.4) |> equal 1 + Convert.ToInt32(1.5) |> equal 2 + Convert.ToInt32(2.5) |> equal 2 + Convert.ToInt32(3.5) |> equal 4 + Convert.ToInt32(-1.5) |> equal -2 + Convert.ToInt32(-2.5) |> equal -2 + Convert.ToInt32(-3.5) |> equal -4 + Convert.ToByte(255.4) |> equal Byte.MaxValue + (fun () -> Convert.ToByte(255.5) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test System.Convert integer conversions check the target range`` () = + Convert.ToSByte(-128) |> equal SByte.MinValue + Convert.ToByte(255) |> equal Byte.MaxValue + Convert.ToInt16(-32768) |> equal Int16.MinValue + Convert.ToUInt16(65535) |> equal UInt16.MaxValue + Convert.ToInt32(2147483647L) |> equal Int32.MaxValue + Convert.ToUInt32(4294967295L) |> equal UInt32.MaxValue + Convert.ToInt64(Int64.MinValue) |> equal Int64.MinValue + Convert.ToUInt64(UInt64.MaxValue) |> equal UInt64.MaxValue + (fun () -> Convert.ToSByte(128) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToByte(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt16(32768) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt16(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt32(2147483648L) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt32(-1L) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToInt64(UInt64.MaxValue) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToUInt64(-1L) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test System.Convert integer conversions handle decimal boolean and char sources`` () = + Convert.ToInt32(1.5M) |> equal 2 + Convert.ToInt32(2.5M) |> equal 2 + Convert.ToInt32(-1.5M) |> equal -2 + Convert.ToInt32(-2.5M) |> equal -2 + Convert.ToByte(255.4M) |> equal Byte.MaxValue + Convert.ToInt32(true) |> equal 1 + Convert.ToInt32(false) |> equal 0 + Convert.ToByte(char 255) |> equal Byte.MaxValue + Convert.ToChar(65535) |> equal Char.MaxValue + (fun () -> Convert.ToByte(255.5M) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToByte(char 256) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToChar(-1) |> ignore) |> throwsOverflow |> equal true + (fun () -> Convert.ToChar(65536) |> ignore) |> throwsOverflow |> equal true + +[] +let ``test FSharp integer casts remain unchecked`` () = + byte 257 |> equal 1uy + sbyte 255 |> equal -1y + [] let ``test System.Convert.ToChar works`` () = let x = 'a' From dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 28 Sep 2026 22:39:08 +0200 Subject: [PATCH 107/322] fix(beam): support Decimal bit constructors (#5007) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 4 +- src/Fable.Transforms/Beam/Replacements.fs | 13 +++++ src/fable-library-beam/fable_decimal.erl | 66 ++++++++++++++++++++++- tests/Beam/ArithmeticTests.fs | 46 +++++++++++++--- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index d610fa5339..ff0f015497 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -159,7 +159,7 @@ also sets `standard_io` and `standard_error` to Unicode. | Signed and unsigned integers | `integer()` | Width restored after operations that can overflow | | `bigint` | `integer()` | No width wrapping | | `float` | `float()` | Erlang floating point | -| `decimal` | scaled `integer()` | Value multiplied by 10^28 | +| `decimal` | scaled `integer()` | Value multiplied by 10^28; `GetBits` emits the smallest equivalent scale | | `string` | UTF-8 `binary()` | Not a charlist | | `char` | `integer()` | Unicode codepoint; see known limitations | | Enum | `integer()` | Casts erase to the underlying number | @@ -464,7 +464,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | Decimal bit constructors, BigInt byte conversion, and special floating-point values need parity work. | +| Numeric APIs | BigInt byte conversion and special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index e2edea5ee3..6cf86330f7 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -1474,6 +1474,19 @@ let private numericTypes (args: Expr list) = match info.CompiledName, thisArg, args with + | ".ctor", None, [ arg ] when info.DeclaringEntityFullName = Types.decimal -> + match arg.Type with + | Type.Array(Type.Number(Int32, _), _) -> + Helper.LibCall(com, "fable_decimal", "from_bits", t, [ derefArr r arg ], ?loc = r) + |> Some + | _ -> None + | ".ctor", None, [ low; mid; high; isNegative; scale ] when info.DeclaringEntityFullName = Types.decimal -> + Helper.LibCall(com, "fable_decimal", "from_parts", t, [ low; mid; high; isNegative; scale ], ?loc = r) + |> Some + | "GetBits", None, [ arg ] when info.DeclaringEntityFullName = Types.decimal -> + Helper.LibCall(com, "fable_decimal", "get_bits", t, [ arg ], ?loc = r) + |> wrapArr com r t + |> Some | "Parse", None, [ arg ] -> match info.DeclaringEntityFullName with | "System.Double" diff --git a/src/fable-library-beam/fable_decimal.erl b/src/fable-library-beam/fable_decimal.erl index 9149707946..b1be6daa02 100644 --- a/src/fable-library-beam/fable_decimal.erl +++ b/src/fable-library-beam/fable_decimal.erl @@ -10,6 +10,8 @@ from_int/1, from_float/1, from_parts/5, + from_bits/1, + get_bits/1, parse/1, try_parse/2, get_one/0, @@ -33,6 +35,8 @@ -spec from_int(integer()) -> integer(). -spec from_float(float() | integer()) -> integer(). -spec from_parts(integer(), integer(), integer(), boolean(), non_neg_integer()) -> integer(). +-spec from_bits([integer()]) -> integer(). +-spec get_bits(integer()) -> [integer()]. -spec parse(binary()) -> integer(). -spec try_parse(binary(), reference()) -> boolean(). -spec get_one() -> integer(). @@ -150,7 +154,7 @@ shift(Value, Exp) -> %% MakeDecimal(low, mid, high, isNegative, scale) %% low/mid/high are signed int32 from .NET — mask to unsigned -from_parts(Low, Mid, High, IsNegative, Scale) -> +from_parts(Low, Mid, High, IsNegative, Scale) when Scale >= 0, Scale =< 28 -> L = Low band 16#FFFFFFFF, M = Mid band 16#FFFFFFFF, H = High band 16#FFFFFFFF, @@ -161,6 +165,66 @@ from_parts(Low, Mid, High, IsNegative, Scale) -> case IsNegative of true -> -Adjusted; false -> Adjusted + end; +from_parts(_Low, _Mid, _High, _IsNegative, Scale) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"Decimal scale must be between 0 and 28.">>, + param_name => <<"scale">>, + actual_value => Scale + }). + +from_bits([Low, Mid, High, Flags]) -> + UnsignedFlags = Flags band 16#FFFFFFFF, + Scale = (UnsignedFlags bsr 16) band 16#FF, + ReservedBits = UnsignedFlags band 16#7F00FFFF, + case ReservedBits =:= 0 andalso Scale =< 28 of + true -> + IsNegative = (UnsignedFlags band 16#80000000) =/= 0, + from_parts(Low, Mid, High, IsNegative, Scale); + false -> + invalid_decimal_bits() + end; +from_bits(_Bits) -> + invalid_decimal_bits(). + +invalid_decimal_bits() -> + erlang:error(#{ + exn_type => argument_exception, + message => <<"Decimal constructor requires four integers with valid sign and scale bits.">> + }). + +%% decision: GetBits emits the smallest scale because fixed-scale values do not retain input scale or negative zero. +%% invariant: every representable .NET decimal round-trips numerically through get_bits/1 and from_bits/1. +get_bits(Value) -> + IsNegative = Value < 0, + {Coefficient, Scale} = canonical_coefficient(abs(Value), 28), + case Coefficient < (1 bsl 96) of + true -> + Low = to_signed_int32(Coefficient), + Mid = to_signed_int32(Coefficient bsr 32), + High = to_signed_int32(Coefficient bsr 64), + Flags = (Scale bsl 16) bor case IsNegative of true -> 16#80000000; false -> 0 end, + [Low, Mid, High, to_signed_int32(Flags)]; + false -> + erlang:error(#{ + exn_type => overflow_exception, + message => <<"Value was either too large or too small for a Decimal.">> + }) + end. + +canonical_coefficient(0, _Scale) -> + {0, 0}; +canonical_coefficient(Coefficient, Scale) when Scale > 0, Coefficient rem 10 =:= 0 -> + canonical_coefficient(Coefficient div 10, Scale - 1); +canonical_coefficient(Coefficient, Scale) -> + {Coefficient, Scale}. + +to_signed_int32(Value) -> + Masked = Value band 16#FFFFFFFF, + case Masked >= 16#80000000 of + true -> Masked - 16#100000000; + false -> Masked end. pow10(0) -> diff --git a/tests/Beam/ArithmeticTests.fs b/tests/Beam/ArithmeticTests.fs index 26cc30e78d..aa6a4fe665 100644 --- a/tests/Beam/ArithmeticTests.fs +++ b/tests/Beam/ArithmeticTests.fs @@ -1056,14 +1056,44 @@ let ``test Decimal floor works`` () = let ``test Decimal pown works`` () = pown 2.2M 3 |> equal 10.648M -// TODO: Decimal constructors from GetBits require low/mid/high int32 representation -// which doesn't map to the fixed-scale integer approach used in Beam. -// [] -// let ``test Decimal constructors work`` () = -// let d = 1.2493M -// let bits = Decimal.GetBits(d) -// let d2 = Decimal(bits) -// d2 |> equal d +[] +let ``test Decimal constructors work`` () = + let d = 1.2493M + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits.[0], bits.[1], bits.[2], false, 4uy) + bits |> equal [| 12493; 0; 0; 262144 |] + d2 |> equal d + d3 |> equal d + +[] +let ``test Decimal GetBits works`` () = + let d = Decimal([| -1; -1; -2; 0 |]) + let bits = Decimal.GetBits(d) + let d2 = Decimal(bits) + let d3 = Decimal(bits.[0], bits.[1], bits.[2], true, 0uy) + bits |> equal [| -1; -1; -2; 0 |] + d2 |> equal d + d3 |> equal -d + +[] +let ``test Decimal bit constructors reject invalid layouts`` () = + throwsAnyError (fun () -> Decimal([| 0; 0; 0 |]) |> ignore) + throwsAnyError (fun () -> Decimal([| 0; 0; 0; 1 |]) |> ignore) + throwsAnyError (fun () -> Decimal(0, 0, 0, false, 29uy) |> ignore) + +[] +let ``test Decimal GetBits works with arithmetic operations`` () = + let check (d: decimal) = + let bits = Decimal.GetBits(d) + Decimal(bits) |> equal d + + check (10M + 10M) + check (100.5M - 0.5M) + check (5M * 200M) + check (-0.1M * 100M) + check (2000M / 10M) + check (1.5M + 0.5M) // TODO: BigInt byte array conversion requires two's complement encoding // which isn't natively supported in Erlang. From 426892ec00cfd847140e598860cb88a19addb8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:16:16 -0700 Subject: [PATCH 108/322] fix(rust): Add proper UnicodeCategory support (#5009) --- src/Fable.Transforms/Rust/Replacements.fs | 4 + src/fable-library-rust/Cargo.toml | 4 +- src/fable-library-rust/src/Char.rs | 388 ++++++++++++++-------- tests/Rust/tests/src/CharTests.fs | 34 ++ tests/Rust/tests/src/RuneTests.fs | 18 +- 5 files changed, 305 insertions(+), 143 deletions(-) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index fc76502f69..c459022ef5 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -1330,8 +1330,12 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio makeLibCall com r t i "Char" meth args |> Some | "ToString", None, [ ExprType(Char) ] -> toString com ctx r args |> Some | "ToString", Some c, [] -> toString com ctx r [ c ] |> Some + | "ToString", Some c, [ _ ] -> toString com ctx r [ c ] |> Some + | ("ToLower" | "ToUpper") as meth, None, [ c; ExprType(DeclaredType(ent, _)) ] when ent.FullName = Types.cultureInfo -> + makeLibCall com r t i "Char" meth [ c ] |> Some | ("ConvertFromUtf32" | "ToLower" | "ToLowerInvariant" | "ToUpper" | "ToUpperInvariant") as meth, None, [ c ] -> makeLibCall com r t i "Char" meth args |> Some + | "GetTypeCode", Some c, [] -> makeLibCall com r t i "Char" "GetTypeCode" [ c ] |> Some | ("TryParse" | "Parse") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair") as meth, None, _ -> $"Rust chars are Unicode scalar values, so surrogate tests will be false." diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index 07618e33c8..f6873211cc 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -18,7 +18,8 @@ reflection = [] regexp = ["dep:regex"] static_do_bindings = ["dep:startup"] threaded = ["atomic", "dep:futures", "dep:futures-timer"] -default = ["bigint", "datetime", "decimal", "enum_func", "enum_string", "guid", "random", "regexp"] +unicode = ["dep:unicode-general-category"] +default = ["bigint", "datetime", "decimal", "enum_func", "enum_string", "guid", "random", "regexp", "unicode"] [dependencies] chrono = { version = "0.4", optional = true } @@ -33,6 +34,7 @@ rand = { version = "0.10", features = ["std_rng"], default-features = false, opt regex = { version = "1.12", optional = true } rust_decimal = { version = "1.42", features = ["maths"], default-features = false, optional = true } startup = { version = "0.1", path = "vendored/startup", optional = true } +unicode-general-category = { version = "1.1", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "1.23", features = ["v4", "v7"], default-features = false, optional = true } diff --git a/src/fable-library-rust/src/Char.rs b/src/fable-library-rust/src/Char.rs index de0e15da59..2018027443 100644 --- a/src/fable-library-rust/src/Char.rs +++ b/src/fable-library-rust/src/Char.rs @@ -1,94 +1,139 @@ pub mod Char_ { - use crate::Native_::{MutCell, ToString, compare}; - use crate::NativeArray_::{Array, array_from}; + use crate::NativeArray_::{array_from, Array}; + use crate::Native_::{compare, MutCell, ToString}; use crate::String_::{getCharAt, length, string, toString}; - - // https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-6.0 - pub mod UnicodeCategory { - pub const UppercaseLetter: u8 = 0; - pub const LowercaseLetter: u8 = 1; - pub const TitlecaseLetter: u8 = 2; - pub const ModifierLetter: u8 = 3; - pub const OtherLetter: u8 = 4; - pub const NonSpacingMark: u8 = 5; - pub const SpacingCombiningMark: u8 = 6; - pub const EnclosingMark: u8 = 7; - pub const DecimalDigitNumber: u8 = 8; - pub const LetterNumber: u8 = 9; - pub const OtherNumber: u8 = 10; - pub const SpaceSeparator: u8 = 11; - pub const LineSeparator: u8 = 12; - pub const ParagraphSeparator: u8 = 13; - pub const Control: u8 = 14; - pub const Format: u8 = 15; - pub const Surrogate: u8 = 16; - pub const PrivateUse: u8 = 17; - pub const ConnectorPunctuation: u8 = 18; - pub const DashPunctuation: u8 = 19; - pub const OpenPunctuation: u8 = 20; - pub const ClosePunctuation: u8 = 21; - pub const InitialQuotePunctuation: u8 = 22; - pub const FinalQuotePunctuation: u8 = 23; - pub const OtherPunctuation: u8 = 24; - pub const MathSymbol: u8 = 25; - pub const CurrencySymbol: u8 = 26; - pub const ModifierSymbol: u8 = 27; - pub const OtherSymbol: u8 = 28; - pub const OtherNotAssigned: u8 = 29; + #[cfg(feature = "unicode")] + use unicode_general_category::{get_general_category, GeneralCategory}; + + // https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[repr(u8)] + pub enum UnicodeCategory { + UppercaseLetter = 0, + LowercaseLetter = 1, + TitlecaseLetter = 2, + ModifierLetter = 3, + OtherLetter = 4, + NonSpacingMark = 5, + SpacingCombiningMark = 6, + EnclosingMark = 7, + DecimalDigitNumber = 8, + LetterNumber = 9, + OtherNumber = 10, + SpaceSeparator = 11, + LineSeparator = 12, + ParagraphSeparator = 13, + Control = 14, + Format = 15, + Surrogate = 16, + PrivateUse = 17, + ConnectorPunctuation = 18, + DashPunctuation = 19, + OpenPunctuation = 20, + ClosePunctuation = 21, + InitialQuotePunctuation = 22, + FinalQuotePunctuation = 23, + OtherPunctuation = 24, + MathSymbol = 25, + CurrencySymbol = 26, + ModifierSymbol = 27, + OtherSymbol = 28, + OtherNotAssigned = 29, + } + + impl core::fmt::Display for UnicodeCategory { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self) + } } + // map GeneralCategory to UnicodeCategory + const UnicodeCategoryMap: [UnicodeCategory; 30] = [ + UnicodeCategory::ClosePunctuation, + UnicodeCategory::ConnectorPunctuation, + UnicodeCategory::Control, + UnicodeCategory::CurrencySymbol, + UnicodeCategory::DashPunctuation, + UnicodeCategory::DecimalDigitNumber, + UnicodeCategory::EnclosingMark, + UnicodeCategory::FinalQuotePunctuation, + UnicodeCategory::Format, + UnicodeCategory::InitialQuotePunctuation, + UnicodeCategory::LetterNumber, + UnicodeCategory::LineSeparator, + UnicodeCategory::LowercaseLetter, + UnicodeCategory::MathSymbol, + UnicodeCategory::ModifierLetter, + UnicodeCategory::ModifierSymbol, + UnicodeCategory::NonSpacingMark, + UnicodeCategory::OpenPunctuation, + UnicodeCategory::OtherLetter, + UnicodeCategory::OtherNumber, + UnicodeCategory::OtherPunctuation, + UnicodeCategory::OtherSymbol, + UnicodeCategory::ParagraphSeparator, + UnicodeCategory::PrivateUse, + UnicodeCategory::SpaceSeparator, + UnicodeCategory::SpacingCombiningMark, + UnicodeCategory::Surrogate, + UnicodeCategory::TitlecaseLetter, + UnicodeCategory::OtherNotAssigned, + UnicodeCategory::UppercaseLetter, + ]; + // The maximum character value. pub const MaxValue: char = '\u{FFFF}'; // The minimum character value. pub const MinValue: char = '\u{0000}'; - // const IsWhiteSpaceFlag: u8 = 0x80; - // const IsUpperCaseLetterFlag: u8 = 0x40; - // const IsLowerCaseLetterFlag: u8 = 0x20; + const IsWhiteSpaceFlag: u8 = 0x80; + const IsUpperCaseLetterFlag: u8 = 0x40; + const IsLowerCaseLetterFlag: u8 = 0x20; const UnicodeCategoryMask: u8 = 0x1F; - // const isControlMask: u32 = 0 | 1 << UnicodeCategory::Control; - // const isDigitMask: u32 = 0 | 1 << UnicodeCategory::DecimalDigitNumber; - // const isLetterMask: u32 = 0 - // | 1 << UnicodeCategory::UppercaseLetter - // | 1 << UnicodeCategory::LowercaseLetter - // | 1 << UnicodeCategory::TitlecaseLetter - // | 1 << UnicodeCategory::ModifierLetter - // | 1 << UnicodeCategory::OtherLetter; - // const isLetterOrDigitMask: u32 = 0 | isLetterMask | isDigitMask; - // const isUpperMask: u32 = 0 | 1 << UnicodeCategory::UppercaseLetter; - // const isLowerMask: u32 = 0 | 1 << UnicodeCategory::LowercaseLetter; - // const isNumberMask: u32 = 0 - // | 1 << UnicodeCategory::DecimalDigitNumber - // | 1 << UnicodeCategory::LetterNumber - // | 1 << UnicodeCategory::OtherNumber; - // const isPunctuationMask: u32 = 0 - // | 1 << UnicodeCategory::ConnectorPunctuation - // | 1 << UnicodeCategory::DashPunctuation - // | 1 << UnicodeCategory::OpenPunctuation - // | 1 << UnicodeCategory::ClosePunctuation - // | 1 << UnicodeCategory::InitialQuotePunctuation - // | 1 << UnicodeCategory::FinalQuotePunctuation - // | 1 << UnicodeCategory::OtherPunctuation; - // const isSeparatorMask: u32 = 0 - // | 1 << UnicodeCategory::SpaceSeparator - // | 1 << UnicodeCategory::LineSeparator - // | 1 << UnicodeCategory::ParagraphSeparator; - // const isSymbolMask: u32 = 0 - // | 1 << UnicodeCategory::MathSymbol - // | 1 << UnicodeCategory::CurrencySymbol - // | 1 << UnicodeCategory::ModifierSymbol - // | 1 << UnicodeCategory::OtherSymbol; - // const isWhiteSpaceMask: u32 = 0 - // | 1 << UnicodeCategory::SpaceSeparator - // | 1 << UnicodeCategory::LineSeparator - // | 1 << UnicodeCategory::ParagraphSeparator; + const isControlMask: u32 = 0 | 1 << UnicodeCategory::Control as u8; + const isDigitMask: u32 = 0 | 1 << UnicodeCategory::DecimalDigitNumber as u8; + const isLetterMask: u32 = 0 + | 1 << UnicodeCategory::UppercaseLetter as u8 + | 1 << UnicodeCategory::LowercaseLetter as u8 + | 1 << UnicodeCategory::TitlecaseLetter as u8 + | 1 << UnicodeCategory::ModifierLetter as u8 + | 1 << UnicodeCategory::OtherLetter as u8; + const isLetterOrDigitMask: u32 = 0 | isLetterMask | isDigitMask; + const isUpperMask: u32 = 0 | 1 << UnicodeCategory::UppercaseLetter as u8; + const isLowerMask: u32 = 0 | 1 << UnicodeCategory::LowercaseLetter as u8; + const isNumberMask: u32 = 0 + | 1 << UnicodeCategory::DecimalDigitNumber as u8 + | 1 << UnicodeCategory::LetterNumber as u8 + | 1 << UnicodeCategory::OtherNumber as u8; + const isPunctuationMask: u32 = 0 + | 1 << UnicodeCategory::ConnectorPunctuation as u8 + | 1 << UnicodeCategory::DashPunctuation as u8 + | 1 << UnicodeCategory::OpenPunctuation as u8 + | 1 << UnicodeCategory::ClosePunctuation as u8 + | 1 << UnicodeCategory::InitialQuotePunctuation as u8 + | 1 << UnicodeCategory::FinalQuotePunctuation as u8 + | 1 << UnicodeCategory::OtherPunctuation as u8; + const isSeparatorMask: u32 = 0 + | 1 << UnicodeCategory::SpaceSeparator as u8 + | 1 << UnicodeCategory::LineSeparator as u8 + | 1 << UnicodeCategory::ParagraphSeparator as u8; + const isSymbolMask: u32 = 0 + | 1 << UnicodeCategory::MathSymbol as u8 + | 1 << UnicodeCategory::CurrencySymbol as u8 + | 1 << UnicodeCategory::ModifierSymbol as u8 + | 1 << UnicodeCategory::OtherSymbol as u8; + const isWhiteSpaceMask: u32 = 0 + | 1 << UnicodeCategory::SpaceSeparator as u8 + | 1 << UnicodeCategory::LineSeparator as u8 + | 1 << UnicodeCategory::ParagraphSeparator as u8; // Contains information about the C0, Basic Latin, C1, and Latin-1 Supplement ranges [ U+0000..U+00FF ], with: // - 0x80 bit if set means 'is whitespace' // - 0x40 bit if set means 'is uppercase letter' // - 0x20 bit if set means 'is lowercase letter' // - bottom 5 bits are the of: UnicodeCategory the character + #[cfg(not(feature = "unicode"))] #[cfg_attr(rustfmt, rustfmt::skip)] const Latin1CharInfo: &[u8; 256] = &[ // 0 1 2 3 4 5 6 7 8 9 A B C D E F @@ -116,27 +161,48 @@ pub mod Char_ { c as u32 <= 0xFF } - // fn isUnicodeCategory(c: char, uc_mask: u32) -> bool { - // let uc = GetUnicodeCategory(c); - // ((1 << uc) & uc_mask) != 0 - // } + #[inline] + fn isUnicodeCategory(c: char, uc_mask: u32) -> bool { + ((1u32 << (GetUnicodeCategory(c) as u32)) & uc_mask) != 0 + } + + #[cfg(feature = "unicode")] + pub fn GetUnicodeCategory(c: char) -> i32 { + let category = get_general_category(c); + UnicodeCategoryMap[category as usize] as i32 + } + #[cfg(not(feature = "unicode"))] pub fn GetUnicodeCategory(c: char) -> i32 { - let category = if (isLatin1(c)) { - (Latin1CharInfo[c as usize] & UnicodeCategoryMask) + if (isLatin1(c)) { + // complete, but only for Latin1 char set + let cat = (Latin1CharInfo[c as usize] & UnicodeCategoryMask); + // let uc: UnicodeCategory = unsafe { std::mem::transmute(cat) }; + cat as i32 } else { - match c { - // very incomplete, TODO: get real unicode categories - c if IsUpper(c) => UnicodeCategory::UppercaseLetter, - c if IsLower(c) => UnicodeCategory::LowercaseLetter, - c if IsLetter(c) => UnicodeCategory::OtherLetter, - c if IsDigit(c) => UnicodeCategory::DecimalDigitNumber, - c if IsControl(c) => UnicodeCategory::Control, + (match c { + // very incomplete, enable the unicode feature to get all unicode categories + c if c.is_uppercase() => UnicodeCategory::UppercaseLetter, + c if c.is_lowercase() => UnicodeCategory::LowercaseLetter, + c if c.is_alphabetic() => match c { + c if c.is_numeric() => UnicodeCategory::LetterNumber, + _ => UnicodeCategory::OtherLetter, + }, + c if c.is_ascii_digit() => UnicodeCategory::DecimalDigitNumber, // incomplete + c if c.is_ascii_punctuation() => UnicodeCategory::ConnectorPunctuation, // incomplete + c if c.is_numeric() => UnicodeCategory::OtherNumber, + c if c.is_control() => UnicodeCategory::Control, + c if c.is_whitespace() => match c { + // only whitespace characters that are not control chars + '\u{2028}' => UnicodeCategory::LineSeparator, + '\u{2029}' => UnicodeCategory::ParagraphSeparator, + _ => UnicodeCategory::SpaceSeparator, + }, c if IsSurrogate(c) => UnicodeCategory::Surrogate, + // other categories are incomplete c => UnicodeCategory::OtherNotAssigned, - } - }; - category as i32 + }) as i32 + } } pub fn GetUnicodeCategory_2(s: string, index: i32) -> i32 { @@ -171,12 +237,26 @@ pub mod Char_ { c as i32 } + pub fn ConvertToUtf32(c1: char, c2: char) -> i32 { + let first = c1 as u32; + let second = c2 as u32; + if (0xD800..=0xDBFF).contains(&first) && (0xDC00..=0xDFFF).contains(&second) { + (0x10000 + ((first - 0xD800) << 10) + (second - 0xDC00)) as i32 + } else { + panic!("The values do not form a valid UTF-16 surrogate pair") + } + } + pub fn GetHashCode(c: char) -> i32 { // Calculate a hashcode for a 2 byte Unicode character. // c as i32 | ((c as i32) << 16) c as i32 } + pub fn GetTypeCode(_c: char) -> i32 { + 4 // TypeCode.Char + } + pub fn Equals(c: char, v: char) -> bool { c == v } @@ -257,88 +337,111 @@ pub mod Char_ { c.is_ascii_hexdigit() && c.is_ascii_uppercase() } + // ---------------------------------------------------- + + #[cfg(feature = "unicode")] + pub fn IsControl(c: char) -> bool { + isUnicodeCategory(c, isControlMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsControl(c: char) -> bool { - // isUnicodeCategory(c, isControlMask) c.is_control() } + #[cfg(feature = "unicode")] pub fn IsDigit(c: char) -> bool { - // if (IsLatin1(c)) { - // matches!(c, '0'..='9') - // } else { - // isUnicodeCategory(c, isDigitMask) - // } - c.is_ascii_digit() + isUnicodeCategory(c, isDigitMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsDigit(c: char) -> bool { + c.is_ascii_digit() // TODO: very incomplete } + #[cfg(feature = "unicode")] pub fn IsLetter(c: char) -> bool { - // if (IsAscii(c)) { - // // For the version of the Unicode standard the type: char is locked to, the - // // ASCII range doesn't include letters in categories other than "upper" and "lower". - // (Latin1CharInfo[c as usize] & (IsUpperCaseLetterFlag | IsLowerCaseLetterFlag)) != 0 - // } else { - // isUnicodeCategory(c, isLetterMask) - // } - c.is_alphabetic() + isUnicodeCategory(c, isLetterMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsLetter(c: char) -> bool { + c.is_alphabetic() // TODO: not precise, includes Marks + } + + #[cfg(feature = "unicode")] + pub fn IsLetterOrDigit(c: char) -> bool { + isUnicodeCategory(c, isLetterOrDigitMask) } + #[cfg(not(feature = "unicode"))] pub fn IsLetterOrDigit(c: char) -> bool { - // isUnicodeCategory(c, LetterOrDigitMask) - c.is_ascii_digit() || c.is_alphabetic() + IsLetter(c) || IsDigit(c) } + #[cfg(feature = "unicode")] + pub fn IsLower(c: char) -> bool { + isUnicodeCategory(c, isLowerMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsLower(c: char) -> bool { - // if (IsLatin1(c)) { - // (Latin1CharInfo[c as usize] & IsLowerCaseLetterFlag) != 0 - // } else { - // isUnicodeCategory(c, isLowerMask) - // } c.is_lowercase() } + #[cfg(feature = "unicode")] + pub fn IsUpper(c: char) -> bool { + isUnicodeCategory(c, isUpperMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsUpper(c: char) -> bool { - // if (IsLatin1(c)) { - // (Latin1CharInfo[c as usize] & IsUpperCaseLetterFlag) != 0 - // } else { - // isUnicodeCategory(c, isUpperMask) - // } c.is_uppercase() } + #[cfg(feature = "unicode")] + pub fn IsNumber(c: char) -> bool { + isUnicodeCategory(c, isNumberMask) + } + + #[cfg(not(feature = "unicode"))] pub fn IsNumber(c: char) -> bool { - // if (IsAscii(c)) { - // matches!(c, '0'..='9') - // } else { - // isUnicodeCategory(c, isNumberMask) - // } c.is_numeric() } - pub fn IsPunctuation(c: char) -> bool { - if (IsAscii(c)) { - c.is_ascii_punctuation() - } else { - c.is_ascii_punctuation() //TODO: imprecise, fix this - // isUnicodeCategory(c, isPunctuationMask) - } + #[cfg(feature = "unicode")] + pub fn IsSeparator(c: char) -> bool { + isUnicodeCategory(c, isSeparatorMask) } + #[cfg(not(feature = "unicode"))] pub fn IsSeparator(c: char) -> bool { - if (isLatin1(c)) { - c == '\u{0020}' || c == '\u{00a0}' - } else { - // isUnicodeCategory(c, isSeparatorMask) - c.is_whitespace() //TODO: imprecise, fix this - } + c.is_whitespace() && !c.is_control() + } + + #[cfg(feature = "unicode")] + pub fn IsPunctuation(c: char) -> bool { + isUnicodeCategory(c, isPunctuationMask) + } + + #[cfg(not(feature = "unicode"))] + pub fn IsPunctuation(c: char) -> bool { + c.is_ascii_punctuation() // TODO: very incomplete + } + + #[cfg(feature = "unicode")] + pub fn IsSymbol(c: char) -> bool { + isUnicodeCategory(c, isSymbolMask) } + #[cfg(not(feature = "unicode"))] pub fn IsSymbol(c: char) -> bool { - // isUnicodeCategory(c, isSymbolMask) - c.is_ascii_punctuation() //TODO: imprecise, fix this + c.is_ascii_punctuation() // TODO: very incomplete } + #[cfg(feature = "unicode")] pub fn IsWhiteSpace(c: char) -> bool { - // if (IsLatin1(c)) { + // if (isLatin1(c)) { // (Latin1CharInfo[c as usize] & IsWhiteSpaceFlag) != 0 // } else { // isUnicodeCategory(c, isWhiteSpaceMask) @@ -346,6 +449,11 @@ pub mod Char_ { c.is_whitespace() } + #[cfg(not(feature = "unicode"))] + pub fn IsWhiteSpace(c: char) -> bool { + c.is_whitespace() + } + // ---------------------------------------------------- pub fn IsControl_2(s: string, index: i32) -> bool { @@ -420,7 +528,7 @@ pub mod Char_ { } pub fn ToUpperInvariant(c: char) -> char { - ToUpper(c) //TODO: use invariant culture + ToUpper(c) // TODO: use invariant culture } pub fn ToLower(c: char) -> char { @@ -438,7 +546,7 @@ pub mod Char_ { } pub fn ToLowerInvariant(c: char) -> char { - ToLower(c) //TODO: use invariant culture + ToLower(c) // TODO: use invariant culture } // ---------------------------------------------------- diff --git a/tests/Rust/tests/src/CharTests.fs b/tests/Rust/tests/src/CharTests.fs index b8de96f1a0..8f9d26a65a 100644 --- a/tests/Rust/tests/src/CharTests.fs +++ b/tests/Rust/tests/src/CharTests.fs @@ -23,6 +23,14 @@ let ``Char.ToUpper with 1-to-many mapping returns original char`` () = let ``Char.ToLower with non-ASCII single mapping works`` () = Char.ToLower('É') |> equal 'é' +[] +let ``Char.ToLower with invariant culture works`` () = + Char.ToLower('É', CultureInfo.InvariantCulture) |> equal 'é' + +[] +let ``Char.ToUpper with invariant culture works`` () = + Char.ToUpper('é', CultureInfo.InvariantCulture) |> equal 'É' + [] let ``Char.ToUpperInvariant works`` () = Char.ToUpperInvariant('b') |> equal 'B' @@ -35,6 +43,18 @@ let ``Char.ToLowerInvariant works`` () = let ``Char.ToString works`` () = Char.ToString('b') |> equal "b" +[] +let ``Char.ToString with format provider works`` () = + 'b'.ToString(CultureInfo.InvariantCulture) |> equal "b" + +[] +let ``Char.GetTypeCode works`` () = + 'b'.GetTypeCode() |> equal TypeCode.Char + +[] +let ``Char.ConvertToUtf32 rejects non-surrogate pairs`` () = + throwsAnyError (fun () -> Char.ConvertToUtf32('a', 'b') |> ignore) + [] let ``Char.GetUnicodeCategory works`` () = Char.GetUnicodeCategory('a') |> equal UnicodeCategory.LowercaseLetter @@ -182,6 +202,20 @@ let ``Char.IsWhitespace works`` () = Char.IsWhiteSpace('\160') |> equal true Char.IsWhiteSpace('-') |> equal false +[] +let ``Char Unicode category predicates work`` () = + Char.IsControl('\u0085') |> equal true + Char.IsDigit('\u0661') |> equal true + Char.IsLetter('Ж') |> equal true + Char.IsLetterOrDigit('\u0661') |> equal true + Char.IsLower('я') |> equal true + Char.IsNumber('\u2167') |> equal true + Char.IsPunctuation('\u201C') |> equal true + Char.IsSeparator('\u2028') |> equal true + Char.IsSymbol('\u20AC') |> equal true + Char.IsUpper('Ж') |> equal true + Char.IsWhiteSpace('\u2007') |> equal true + [] let ``Char.IsWhitespace works with two args`` () = let input = " \r" diff --git a/tests/Rust/tests/src/RuneTests.fs b/tests/Rust/tests/src/RuneTests.fs index 633876b4f7..cfb2b2223a 100644 --- a/tests/Rust/tests/src/RuneTests.fs +++ b/tests/Rust/tests/src/RuneTests.fs @@ -159,7 +159,21 @@ let ``Rune.IsWhiteSpace works`` () = Rune.IsWhiteSpace(Rune('\n')) |> equal true [] -let ``Rune.ToLower works`` () = +let ``Rune Unicode category predicates work`` () = + Rune.IsControl(Rune('\u0085')) |> equal true + Rune.IsDigit(Rune('\u0661')) |> equal true + Rune.IsLetter(Rune('Ж')) |> equal true + Rune.IsLetterOrDigit(Rune('\u0661')) |> equal true + Rune.IsLower(Rune('я')) |> equal true + Rune.IsNumber(Rune('\u2167')) |> equal true + Rune.IsPunctuation(Rune('\u201C')) |> equal true + Rune.IsSeparator(Rune('\u2028')) |> equal true + Rune.IsSymbol(Rune('\u20AC')) |> equal true + Rune.IsUpper(Rune('Ж')) |> equal true + Rune.IsWhiteSpace(Rune('\u2007')) |> equal true + +[] +let ``Rune.ToLower with invariant culture works`` () = Rune.ToLower(Rune('A'), CultureInfo.InvariantCulture) |> equal (Rune('a')) [] @@ -167,7 +181,7 @@ let ``Rune.ToLowerInvariant works`` () = Rune.ToLowerInvariant(Rune('A')) |> equal (Rune('a')) [] -let ``Rune.ToUpper works`` () = +let ``Rune.ToUpper with invariant culture works`` () = Rune.ToUpper(Rune('b'), CultureInfo.InvariantCulture) |> equal (Rune('B')) [] From 01eeded22f292e77c7c6c024de5d894d705fa09d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 29 Sep 2026 20:16:31 +0200 Subject: [PATCH 109/322] fix(beam): share cancellation state across processes (#5010) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 13 + src/Fable.Transforms/Beam/Replacements.fs | 9 +- src/fable-library-beam/fable_async.erl | 120 +++++-- src/fable-library-beam/fable_cancellation.erl | 302 ++++++++++++++---- tests/Beam/AsyncTests.fs | 229 ++++++++++++- 5 files changed, 581 insertions(+), 92 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index ff0f015497..cb12f7bf52 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -361,6 +361,18 @@ composition execute in the caller's process so process-local mutable state remai visible. `Async.Parallel`, `Async.StartChild`, and parallel array operations spawn workers; those workers do not share the caller's process dictionary. +Cancellation tokens use shared `atomics` state and can cross process boundaries. +Registration, cancellation, and `CancelAfter` are serialized by one runtime broker. +`Cancel()` invokes callbacks in the cancelling process; `CancelAfter` uses a +short-lived worker. A cross-process callback must therefore use process-safe state +such as PIDs and messages rather than captured process-dictionary-backed mutables. +Dispose a registration when its source may never cancel, because the broker retains +active callbacks until cancellation or disposal. + +`Async.StartChild` and `Async.Parallel` do not implicitly propagate their parent +cancellation token. Pass or capture a token explicitly when a spawned computation +must observe it. + `Task` uses the same runtime and is not a separate hot-task abstraction. `MailboxProcessor` also uses in-process CPS and a process-local queue. Real OTP processes, `gen_server`, supervisors, ETS, and distribution come from external @@ -469,6 +481,7 @@ suite grows. | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | | Recursive values | Recursive value bindings that lower through `Lazy` and some inline module-value side effects are incomplete. | +| Cancellation | Cross-process callbacks cannot safely mutate captured process-local values. Callback exceptions are suppressed, and `CancellationTokenSource.Dispose()` remains a no-op. | | Diagnostics | Unhandled Fable value kinds and unsupported assignment shapes can still compile to runtime `erlang:error({unsupported_*})` paths instead of failing compilation. | For `char` conversion in generic code, making the function `inline` or using a diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 6cf86330f7..a9f05d993f 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -3217,6 +3217,12 @@ let cancels (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr op ?loc = r ) |> Some + | "Dispose" when i.DeclaringEntityFullName = "System.Threading.CancellationTokenRegistration" -> + match thisArg with + | Some registration -> + Helper.LibCall(com, "fable_utils", "safe_dispose", t, [ registration ], ?loc = r) + |> Some + | None -> None | "Dispose" -> Null Type.Unit |> makeValue r |> Some | "Register" -> let args, argTypes = @@ -5835,7 +5841,8 @@ let tryCall | Types.printfModule | Naming.StartsWith Types.printfFormat _ -> fsFormat com ctx r t info thisArg args | "System.Threading.CancellationToken" - | "System.Threading.CancellationTokenSource" -> cancels com ctx r t info thisArg args + | "System.Threading.CancellationTokenSource" + | "System.Threading.CancellationTokenRegistration" -> cancels com ctx r t info thisArg args | "Microsoft.FSharp.Control.CommonExtensions" -> controlExtensions com ctx r t info thisArg args | "Microsoft.FSharp.Control.FSharpEvent`1" | "Microsoft.FSharp.Control.FSharpEvent`2" diff --git a/src/fable-library-beam/fable_async.erl b/src/fable-library-beam/fable_async.erl index 85b1e466b7..0c2fe5bb16 100644 --- a/src/fable-library-beam/fable_async.erl +++ b/src/fable-library-beam/fable_async.erl @@ -24,7 +24,8 @@ on_success := fun(), on_error := fun(), on_cancel := fun(), - cancel_token := reference() | undefined + cancel_token := reference() | undefined, + wait_for_async := boolean() }. -type async(T) :: fun((async_ctx()) -> T). @@ -97,7 +98,8 @@ default_ctx(CancelToken) -> on_success => fun(_) -> ok end, on_error => fun(E) -> erlang:error(E) end, on_cancel => fun(_) -> ok end, - cancel_token => CancelToken + cancel_token => CancelToken, + wait_for_async => false }. %% StartImmediate: run with default context (fire-and-forget) @@ -120,10 +122,9 @@ start_immediate(Computation, CancelToken) -> %% it is killed (e.g. on timeout) or the VM stops, so a child whose result is %% never awaited leaves an idle process behind. %% -%% Cancellation tokens are intentionally not propagated to the child: tokens are -%% held in the process dictionary (see fable_cancellation), so they are only -%% meaningful within a single process and cannot be shared with the child's -%% separate process. This matches parallel/1. +%% StartChild does not currently propagate its parent context's token. Tokens are +%% process-portable, but changing combinator propagation is a separate semantic +%% change from making an explicitly supplied or captured token safe to share. start_child(Computation) -> start_child(Computation, undefined). start_child(Computation, Timeout) -> fun(Ctx) -> @@ -185,7 +186,10 @@ run_synchronously(Computation, CancelToken) -> on_success => fun(V) -> put(Ref, {ok, V}) end, on_error => fun(E) -> put(Ref, {error, E}) end, on_cancel => fun(_) -> put(Ref, {cancelled}) end, - cancel_token => CancelToken + cancel_token => CancelToken, + %% invariant: runtime-owned callbacks are handled by the process that created their + %% process-dictionary-backed continuations. + wait_for_async => true }, try Computation(Ctx) @@ -216,7 +220,8 @@ start_with_continuations(Comp, OnSuccess, OnError, OnCancel, Token) -> on_success => OnSuccess, on_error => OnError, on_cancel => OnCancel, - cancel_token => Token + cancel_token => Token, + wait_for_async => false }, try Comp(Ctx) @@ -243,7 +248,7 @@ sleep(Milliseconds) -> %% Set up a timer to send us a wake-up message Self = self(), TimerRef = make_ref(), - timer:apply_after(Milliseconds, erlang, send, [Self, {sleep_done, TimerRef}]), + SleepTimer = erlang:send_after(Milliseconds, Self, {sleep_done, TimerRef}), %% Register a cancellation listener to wake us up early RegId = fable_cancellation:register(Token, fun(_) -> Self ! {sleep_cancelled, TimerRef} @@ -253,17 +258,10 @@ sleep(Milliseconds) -> {sleep_done, TimerRef} -> (maps:get(on_success, Ctx))(ok); {sleep_cancelled, TimerRef} -> - (maps:get(on_cancel, Ctx))(ok); - {cancel_token, Token} -> - %% Timer-based cancel via cancel_after - fable_cancellation:cancel(Token), (maps:get(on_cancel, Ctx))(ok) end, - %% Clean up registration if we have one - case RegId of - undefined -> ok; - _ -> ok - end + erlang:cancel_timer(SleepTimer), + fable_utils:safe_dispose(RegId) end end end. @@ -275,28 +273,92 @@ await_event(Event, CancelAction) -> OnSuccess = maps:get(on_success, Ctx), OnCancel = maps:get(on_cancel, Ctx), Token = maps:get(cancel_token, Ctx), + Owner = self(), + AwaitRef = make_ref(), + RegistrationRef = make_ref(), + put(AwaitRef, pending), + put(RegistrationRef, undefined), Handler = fun F(_Sender, Value) -> - (maps:get(remove_handler, Event))(F), - OnSuccess(Value) + case self() =:= Owner of + true -> + finish_await_event( + {success, Value}, Event, F, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ); + false -> + Owner ! {fable_async_await_event, AwaitRef, {success, Value}} + end end, - case Token of - undefined -> ok; + %% Subscribe before registering cancellation so an already-cancelled token can remove the + %% handler synchronously instead of leaving a handler that can never complete successfully. + (maps:get(add_handler, Event))(Handler), + Registration = case Token of + undefined -> undefined; _ -> fable_cancellation:register(Token, fun(_) -> - (maps:get(remove_handler, Event))(Handler), - case CancelAction of - undefined -> ok; - _ -> CancelAction(ok) - end, - OnCancel(ok) + case self() =:= Owner of + true -> + finish_await_event( + cancelled, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ); + false -> + Owner ! {fable_async_await_event, AwaitRef, cancelled} + end end) end, + case get(AwaitRef) of + pending -> put(RegistrationRef, Registration); + _ -> fable_utils:safe_dispose(Registration) + end, - (maps:get(add_handler, Event))(Handler) + case maps:get(wait_for_async, Ctx, false) andalso get(AwaitRef) =:= pending of + true -> + receive + {fable_async_await_event, AwaitRef, Result} -> + finish_await_event( + Result, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef + ) + end; + false -> ok + end end. +finish_await_event( + Result, Event, Handler, Token, CancelAction, + OnSuccess, OnCancel, AwaitRef, RegistrationRef +) -> + case get(AwaitRef) of + pending -> + put(AwaitRef, completed), + (maps:get(remove_handler, Event))(Handler), + fable_utils:safe_dispose(get(RegistrationRef)), + erase(AwaitRef), + erase(RegistrationRef), + case Result of + {success, Value} -> + %% Cancellation that has already won the broker race must also win an event + %% delivery racing with its owner-process message. + case fable_cancellation:is_cancellation_requested(Token) of + true -> invoke_await_event_cancel(CancelAction, OnCancel); + false -> OnSuccess(Value) + end; + cancelled -> + invoke_await_event_cancel(CancelAction, OnCancel) + end; + _ -> ok + end. + +invoke_await_event_cancel(CancelAction, OnCancel) -> + case CancelAction of + undefined -> ok; + _ -> CancelAction(ok) + end, + OnCancel(ok). + %% Parallel: spawn one process per computation, collect results in order parallel(Computations) when is_reference(Computations) -> parallel(get(Computations)); diff --git a/src/fable-library-beam/fable_cancellation.erl b/src/fable-library-beam/fable_cancellation.erl index e5f339592d..beab9ee665 100644 --- a/src/fable-library-beam/fable_cancellation.erl +++ b/src/fable-library-beam/fable_cancellation.erl @@ -14,25 +14,33 @@ -spec cancel_after(reference() | undefined, non_neg_integer()) -> ok. -spec is_cancellation_requested(reference() | undefined) -> boolean(). -spec throw_if_cancellation_requested(reference() | undefined) -> ok. --spec register(reference() | undefined, fun()) -> {reference(), integer()} | undefined. --spec register(reference() | undefined, fun(), term()) -> {reference(), integer()} | undefined. +-spec register(reference() | undefined, fun()) -> map() | undefined. +-spec register(reference() | undefined, fun(), term()) -> map() | undefined. + +-define(SERVER, fable_cancellation_server). +-define(CALLBACK_KEY, '$fable_cancellation_callback'). %% CancellationToken/CancellationTokenSource runtime. -%% Token = make_ref(), stored in process dict as: -%% #{cancelled => false, listeners => #{}, next_id => 1} %% -%% Uses process dict pattern (same as Ref/Dictionary/ResizeArray). -%% Since Beam async runs same-process CPS, no cross-process communication needed. +%% decision: stores the cancellation bit in atomics so token state is shared and garbage-collected +%% with the token instead of being tied to any BEAM process or ETS owner +%% decision: serializes registration, cancellation, and timers through one runtime-wide broker — +%% this closes the register-versus-cancel race without leaking one process per token +%% invariant: the broker never invokes a callback for direct cancellation — Cancel runs callbacks +%% synchronously in its caller, matching CancellationTokenSource.Cancel semantics +%% invariant: a running registration remains broker-owned until its callback completes — Dispose +%% can therefore wait for callbacks executing in another process +%% tradeoff: user callbacks cannot capture process-dictionary-backed Fable mutable values across +%% processes because Cancel runs them in its caller and CancelAfter uses a short-lived worker; +%% runtime-owned callbacks must marshal their work back to the registering process create() -> create(undefined). create(undefined) -> - Token = make_ref(), - put(Token, #{cancelled => false, listeners => #{}, next_id => 1}), - Token; + atomics:new(1, []); create(true) -> - Token = make_ref(), - put(Token, #{cancelled => true, listeners => #{}, next_id => 1}), + Token = create(undefined), + atomics:put(Token, 1, 1), Token; create(false) -> create(undefined); @@ -44,78 +52,252 @@ create(Ms) when is_integer(Ms) -> cancel(undefined) -> ok; cancel(Token) -> - State = get(Token), - put(Token, State#{cancelled := true}), - invoke_listeners(maps:get(listeners, State)), - ok. + case call({cancel, Token}) of + {invoke, Listeners} -> invoke_listeners(Token, Listeners); + already_cancelled -> ok + end. cancel_after(undefined, _Ms) -> ok; cancel_after(Token, Ms) -> - Self = self(), - timer:apply_after(Ms, erlang, send, [Self, {cancel_token, Token}]), - %% Spawn a receiver that watches for the cancel message - %% Since we're same-process CPS, we set up a process dict flag - %% and check it. But for timer-based cancel we need a receive loop. - %% Use a helper process that sends us a message we handle in sleep. + call({cancel_after, Token, Ms}), ok. is_cancellation_requested(undefined) -> false; is_cancellation_requested(Token) -> - drain_cancel_messages(), - maps:get(cancelled, get(Token)). + atomics:get(Token, 1) =:= 1. throw_if_cancellation_requested(undefined) -> ok; throw_if_cancellation_requested(Token) -> - drain_cancel_messages(), - case maps:get(cancelled, get(Token)) of + case is_cancellation_requested(Token) of true -> erlang:error(operation_cancelled); false -> ok end. -register(Token, F) -> register(Token, F, undefined). +register(Token, F) -> register(Token, F, ok). register(undefined, _F, _State) -> undefined; -register(Token, F, _State) -> - State = get(Token), - Id = maps:get(next_id, State), - Listeners = maps:get(listeners, State), - put(Token, State#{listeners := Listeners#{Id => F}, next_id := Id + 1}), - %% Return a disposable-like value (the registration id + token) - {Token, Id}. +register(Token, F, State) -> + Id = make_ref(), + case call({register, Token, Id, F, State}) of + registered -> + #{dispose => fun(_) -> unregister(Token, Id) end}; + invoke -> + invoke_listener({F, State}), + #{dispose => fun(_) -> ok end} + end. + +unregister(Token, Id) -> + IsCurrentCallback = get(?CALLBACK_KEY) =:= {Token, Id}, + call({unregister, Token, Id, IsCurrentCallback}), + ok. %% Internal helpers -invoke_listeners(Listeners) -> - maps:foreach( - fun(_Id, F) -> - try - F(ok) - catch - _:_ -> ok - end - end, - Listeners - ). - -%% Drain any pending {cancel_token, Token} messages from the mailbox -%% and apply cancellation. This is needed for timer-based cancellation -%% since timer:apply_after sends a message to self(). -drain_cancel_messages() -> +invoke_listeners(Token, Listeners) -> + lists:foreach(fun(Listener) -> invoke_listener(Token, Listener) end, Listeners). + +invoke_listener(Token, {Id, F, State}) -> + PreviousCallback = put(?CALLBACK_KEY, {Token, Id}), + invoke_listener({F, State}), + case PreviousCallback of + undefined -> erase(?CALLBACK_KEY); + _ -> put(?CALLBACK_KEY, PreviousCallback) + end, + call({callback_complete, Token, Id}), + ok. + +invoke_listener({F, State}) -> + try + F(State) + catch + _:_ -> ok + end. + +call(Request) -> + Server = server(), + Ref = make_ref(), + Monitor = erlang:monitor(process, Server), + Server ! {call, self(), Ref, Request}, + receive + {Ref, Reply} -> + erlang:demonitor(Monitor, [flush]), + Reply; + {'DOWN', Monitor, process, Server, Reason} -> + erlang:error({cancellation_server_stopped, Reason}) + end. + +server() -> + case whereis(?SERVER) of + undefined -> start_server(); + Pid -> Pid + end. + +start_server() -> + Pid = spawn(fun() -> server_loop(#{}) end), + try erlang:register(?SERVER, Pid) of + true -> Pid + catch + error:badarg -> + exit(Pid, kill), + server() + end. + +server_loop(Entries) -> receive - {cancel_token, Token} -> - State = get(Token), - case maps:get(cancelled, State) of - true -> - ok; - false -> - put(Token, State#{cancelled := true}), - invoke_listeners(maps:get(listeners, State)) + {call, From, Ref, {register, Token, Id, F, State}} -> + case atomics:get(Token, 1) of + 1 -> + From ! {Ref, invoke}, + server_loop(Entries); + 0 -> + Entry = maps:get(Token, Entries, new_entry()), + Listeners = maps:get(listeners, Entry), + Entry1 = Entry#{listeners := Listeners#{Id => {F, State}}}, + From ! {Ref, registered}, + server_loop(Entries#{Token => Entry1}) + end; + {call, From, Ref, {unregister, Token, Id, IsCurrentCallback}} -> + {Entries1, Reply} = unregister_listener( + Token, Id, From, Ref, IsCurrentCallback, Entries + ), + case Reply of + now -> From ! {Ref, ok}; + after_callback -> ok end, - drain_cancel_messages() - after 0 -> - ok + server_loop(Entries1); + {call, From, Ref, {cancel, Token}} -> + case atomics:compare_exchange(Token, 1, 0, 1) of + ok -> + {Listeners, Entries1} = begin_callbacks(Token, From, Entries), + From ! {Ref, {invoke, Listeners}}, + server_loop(Entries1); + 1 -> + From ! {Ref, already_cancelled}, + server_loop(Entries) + end; + {call, From, Ref, {cancel_after, Token, Ms}} -> + Entries1 = schedule_cancel_after(Token, Ms, Entries), + From ! {Ref, ok}, + server_loop(Entries1); + {cancel_after, Token, Generation} -> + case maps:find(Token, Entries) of + {ok, #{timer := {_TimerRef, Generation}}} -> + case atomics:compare_exchange(Token, 1, 0, 1) of + ok -> + %% CancelAfter has no calling process in which callbacks can run. + %% A short-lived worker keeps user code out of the state broker. + Worker = spawn(fun() -> + receive + {invoke, Listeners} -> invoke_listeners(Token, Listeners) + end + end), + {Listeners, Entries1} = begin_callbacks(Token, Worker, Entries), + Worker ! {invoke, Listeners}, + server_loop(Entries1); + 1 -> + Entries1 = remove_timer(Token, Entries), + server_loop(Entries1) + end; + _ -> + server_loop(Entries) + end; + {call, From, Ref, {callback_complete, Token, Id}} -> + Entries1 = complete_callback(Token, Id, Entries), + From ! {Ref, ok}, + server_loop(Entries1) + end. + +new_entry() -> + #{listeners => #{}, running => #{}, waiters => #{}, timer => undefined}. + +schedule_cancel_after(Token, Ms, Entries) -> + case atomics:get(Token, 1) of + 1 -> Entries; + 0 -> + Entry = maps:get(Token, Entries, new_entry()), + cancel_timer(maps:get(timer, Entry)), + Generation = make_ref(), + TimerRef = erlang:send_after(Ms, self(), {cancel_after, Token, Generation}), + Entries#{Token => Entry#{timer := {TimerRef, Generation}}} + end. + +update_entry(Token, F, Entries) -> + case maps:find(Token, Entries) of + error -> Entries; + {ok, Entry} -> + Entry1 = F(Entry), + case entry_empty(Entry1) of + true -> maps:remove(Token, Entries); + false -> Entries#{Token := Entry1} + end + end. + +begin_callbacks(Token, Owner, Entries) -> + case maps:find(Token, Entries) of + error -> {[], Entries}; + {ok, Entry} -> + cancel_timer(maps:get(timer, Entry)), + Listeners = maps:get(listeners, Entry), + Running = maps:from_list([{Id, Owner} || Id <- maps:keys(Listeners)]), + Callbacks = [{Id, F, State} || {Id, {F, State}} <- maps:to_list(Listeners)], + Entry1 = Entry#{listeners := #{}, running := Running, timer := undefined}, + {Callbacks, put_entry(Token, Entry1, Entries)} + end. + +unregister_listener(Token, Id, From, Ref, IsCurrentCallback, Entries) -> + case maps:find(Token, Entries) of + error -> {Entries, now}; + {ok, Entry} -> + Listeners = maps:get(listeners, Entry), + Running = maps:get(running, Entry), + case maps:find(Id, Running) of + {ok, From} when IsCurrentCallback -> + %% A callback may dispose its own registration; waiting here would deadlock. + {Entries, now}; + {ok, _Owner} -> + Waiters = maps:get(waiters, Entry), + RegistrationWaiters = maps:get(Id, Waiters, []), + Entry1 = Entry#{waiters := Waiters#{Id => [{From, Ref} | RegistrationWaiters]}}, + {Entries#{Token := Entry1}, after_callback}; + error -> + Entry1 = Entry#{listeners := maps:remove(Id, Listeners)}, + {put_entry(Token, Entry1, Entries), now} + end + end. + +complete_callback(Token, Id, Entries) -> + case maps:find(Token, Entries) of + error -> Entries; + {ok, Entry} -> + Waiters = maps:get(waiters, Entry), + lists:foreach(fun({Pid, Ref}) -> Pid ! {Ref, ok} end, maps:get(Id, Waiters, [])), + Entry1 = Entry#{ + running := maps:remove(Id, maps:get(running, Entry)), + waiters := maps:remove(Id, Waiters) + }, + put_entry(Token, Entry1, Entries) + end. + +remove_timer(Token, Entries) -> + update_entry(Token, fun(Entry) -> Entry#{timer := undefined} end, Entries). + +put_entry(Token, Entry, Entries) -> + case entry_empty(Entry) of + true -> maps:remove(Token, Entries); + false -> Entries#{Token => Entry} end. + +entry_empty(#{listeners := Listeners, running := Running, waiters := Waiters, timer := Timer}) -> + map_size(Listeners) =:= 0 andalso + map_size(Running) =:= 0 andalso + map_size(Waiters) =:= 0 andalso + Timer =:= undefined. + +cancel_timer(undefined) -> ok; +cancel_timer({TimerRef, _Generation}) -> + erlang:cancel_timer(TimerRef), + ok. diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index 46ad509762..f1ebea0b34 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -428,6 +428,203 @@ let ``test Unit arguments are erased`` () = #if FABLE_COMPILER +open Fable.Core.BeamInterop + +type private CancellationProbeMessage = + | CancellationProbeReady of worker: obj + | CancellationProbeContinue + | CancellationCallbackInvoked + | CancellationCallbackStarted of worker: obj + | CancellationDisposeReturned + +// Run a thunk in a fresh BEAM process and return its result to the caller. +[ CancellationParent = self(), CancellationFun = $0, spawn(fun() -> CancellationParent ! {cancellation_result, case erlang:fun_info(CancellationFun, arity) of {arity, 0} -> CancellationFun(); _ -> CancellationFun(ok) end} end), receive {cancellation_result, CancellationResult} -> CancellationResult after 1000 -> timeout end end)()")>] +let private runInCancellationProcess (f: unit -> 'a) : 'a = Fable.Core.Util.nativeOnly + +[ CancellationFun = $0, spawn(fun() -> case erlang:fun_info(CancellationFun, arity) of {arity, 0} -> CancellationFun(); _ -> CancellationFun(ok) end end) end)()")>] +let private startCancellationProcess (f: unit -> unit) : obj = Fable.Core.Util.nativeOnly + +[] +let private runSynchronouslyWithCancellation (_workflow: Async<'a>) (_token: System.Threading.CancellationToken) : 'a = + Fable.Core.Util.nativeOnly + +let private sendCancellationReady (parent: obj) = + let worker: obj = emitErlExpr () "erlang:self()" + emitErlExpr (parent, worker) "$0 ! {cancellation_probe_ready, $1}" + +let private waitForCancellationReady () = + match Erlang.receive 1000 with + | Some(CancellationProbeReady worker) -> Some worker + | _ -> None + +[] +let ``test cancellation token state crosses process boundaries`` () = + let cts = new System.Threading.CancellationTokenSource() + + runInCancellationProcess (fun () -> cts.Token.IsCancellationRequested) + |> equal false + +[] +let ``test running process observes cancellation from source process`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + sendCancellationReady parent + + match Erlang.receive 1000 with + | Some CancellationProbeContinue -> return cts.Token.IsCancellationRequested + | _ -> return false + }) + + match waitForCancellationReady () with + | Some worker -> + cts.Cancel() + emitErlExpr worker "$0 ! cancellation_probe_continue" + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test Async Sleep in another process is interrupted by live cancellation`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + let wasCancelledBeforeStart = cts.Token.IsCancellationRequested + sendCancellationReady parent + let mutable cancelled = false + + Async.StartWithContinuations( + async { do! Async.Sleep 1000 }, + ignore, + ignore, + (fun _ -> cancelled <- true), + cts.Token + ) + + return + not wasCancelledBeforeStart + && cancelled + && cts.Token.IsCancellationRequested + }) + + match waitForCancellationReady () with + | Some _ -> + cts.Cancel() + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test cross-process cancellation registration runs once`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + cts.Token.Register(fun () -> + emitErlExpr parent "$0 ! cancellation_callback_invoked" + ) + |> ignore + + sendCancellationReady parent + + match Erlang.receive 1000 with + | Some CancellationProbeContinue -> return true + | _ -> return false + }) + + match waitForCancellationReady () with + | Some worker -> + cts.Cancel() + cts.Cancel() + + let invokedOnce = + match Erlang.receive 1000 with + | Some CancellationCallbackInvoked -> + Erlang.receive 0 |> Option.isNone + | _ -> false + + emitErlExpr worker "$0 ! cancellation_probe_continue" + let! childCompleted = child + return invokedOnce && childCompleted + | None -> return false + } + + Async.RunSynchronously comp |> equal true + +[] +let ``test CancellationToken registration Dispose waits for a running callback`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + let registration = + cts.Token.Register(fun () -> + let worker: obj = emitErlExpr () "erlang:self()" + emitErlExpr (parent, worker) "$0 ! {cancellation_callback_started, $1}" + Erlang.receive 1000 |> ignore + ) + + startCancellationProcess (fun () -> cts.Cancel()) |> ignore + + match Erlang.receive 1000 with + | Some(CancellationCallbackStarted worker) -> + startCancellationProcess (fun () -> + registration.Dispose() + emitErlExpr parent "$0 ! cancellation_dispose_returned" + ) |> ignore + + Erlang.receive 0 |> Option.isNone |> equal true + emitErlExpr worker "$0 ! cancellation_probe_continue" + + match Erlang.receive 1000 with + | Some CancellationDisposeReturned -> () + | _ -> failwith "Dispose did not return after the callback completed" + | _ -> failwith "Cancellation callback did not start" + +[] +let ``test Async AwaitEvent cancellation runs in the event owner process`` () = + let cts = new System.Threading.CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + + let comp = async { + let! child = + Async.StartChild(async { + let ev = Event() + let mutable cancelCalled = false + sendCancellationReady parent + + let wasCancelled = + try + runSynchronouslyWithCancellation + (async { + let! _ = Async.AwaitEvent(ev.Publish, fun () -> cancelCalled <- true) + return false + }) + cts.Token + with _ -> true + + return wasCancelled && cancelCalled + }) + + match waitForCancellationReady () with + | Some _ -> + cts.Cancel() + return! child + | None -> return false + } + + Async.RunSynchronously comp |> equal true + [] let ``test CancellationTokenSource create and cancel works`` () = let tcs = new System.Threading.CancellationTokenSource() @@ -459,6 +656,27 @@ let ``test CancellationTokenSource multiple registers work`` () = tcs.Cancel() equal 11 x +[] +let ``test CancellationToken registration after cancellation runs immediately`` () = + let mutable calls = 0 + let cts = new System.Threading.CancellationTokenSource() + cts.Cancel() + + cts.Token.Register(fun () -> calls <- calls + 1) |> ignore + + equal 1 calls + +[] +let ``test CancellationToken registration can be disposed`` () = + let mutable calls = 0 + let cts = new System.Threading.CancellationTokenSource() + let registration = cts.Token.Register(fun () -> calls <- calls + 1) + registration.Dispose() + + cts.Cancel() + + equal 0 calls + [] let ``test async cancellation with pre-cancelled token`` () = let mutable res = false @@ -513,11 +731,18 @@ let ``test Can use custom exceptions in async workflows`` () = [] let ``test Async.AwaitEvent fires continuation when event is triggered`` () = let ev = Event() + let cts = new System.Threading.CancellationTokenSource() + let mutable cancelCalled = false + let mutable completed = false Async.StartImmediate(async { - let! v = Async.AwaitEvent ev.Publish + let! v = Async.AwaitEvent(ev.Publish, fun () -> cancelCalled <- true) equal 42 v - }) + completed <- true + }, cts.Token) ev.Trigger(42) + cts.Cancel() + equal true completed + equal false cancelCalled [] let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = From 358548d8c6d55c719b8bb24cccd2c91b27e4b6d4 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 29 Sep 2026 22:06:25 +0200 Subject: [PATCH 110/322] fix(beam): close BigInteger API gaps (#5011) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 2 +- src/Fable.Transforms/Beam/Replacements.fs | 17 +++++- src/fable-library-beam/fable_int.erl | 67 ++++++++++++++++++++++- tests/Beam/ArithmeticTests.fs | 54 +++++++++++++++--- 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index cb12f7bf52..20baa0898e 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -476,7 +476,7 @@ suite grows. | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | -| Numeric APIs | BigInt byte conversion and special floating-point values need parity work. | +| Numeric APIs | Special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Hashing | Array hashing currently follows content rather than .NET reference identity. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index a9f05d993f..ead36e8f0b 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5740,7 +5740,12 @@ let tryCall | "Microsoft.FSharp.Core.NumericLiterals.NumericLiteralI" -> // Erlang has native arbitrary-precision integers, so BigInt ops map directly match info.CompiledName, thisArg, args with - | ".ctor", None, [ arg ] -> toBigIntFrom r t arg |> Some // bigint(x) = x in Erlang, bar a float + | ".ctor", None, [ arg ] -> + match arg.Type with + | Array(Number(UInt8, _), _) -> + Helper.LibCall(com, "fable_int", "bigint_from_byte_array", t, [ arg ], ?loc = r) + |> Some + | _ -> toBigIntFrom r t arg |> Some // bigint(x) = x in Erlang, bar a float | "op_Addition", None, [ left; right ] -> makeBinOp r t left right BinaryPlus |> Some | "op_Subtraction", None, [ left; right ] -> makeBinOp r t left right BinaryMinus |> Some | "op_Multiply", None, [ left; right ] -> makeBinOp r t left right BinaryMultiply |> Some @@ -5773,7 +5778,15 @@ let tryCall | Number(BigInt, _) -> toBigIntFrom r t arg |> Some | _ -> arg |> wrapToIntType com r t arg.Type |> Some | "Parse", None, [ str ] -> emitExpr r t [ str ] "binary_to_integer($0)" |> Some - | "Pow", None, [ base_; exp_ ] -> emitExpr r t [ base_; exp_ ] "math:pow($0, $1)" |> Some + | "ToByteArray", Some value, [] -> + Helper.LibCall(com, "fable_int", "bigint_to_byte_array", t, [ value ], ?loc = r) + |> Some + | "Pow", None, [ base_; exp_ ] -> + Helper.LibCall(com, "fable_int", "bigint_pow", t, [ base_; exp_ ], ?loc = r) + |> Some + | "GreatestCommonDivisor", None, [ left; right ] -> + Helper.LibCall(com, "fable_int", "bigint_gcd", t, [ left; right ], ?loc = r) + |> Some | "Log", None, [ arg ] -> emitExpr r t [ arg ] "math:log(float($0))" |> Some | "Log", None, [ arg; base_ ] -> emitExpr r t [ arg; base_ ] "(math:log(float($0)) / math:log($1))" |> Some | "Log10", None, [ arg ] -> emitExpr r t [ arg ] "math:log10(float($0))" |> Some diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 8e71213c08..9113fa354b 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -8,7 +8,11 @@ wrap_u16/1, wrap_u32/1, wrap_u64/1, - log2/1 + log2/1, + bigint_from_byte_array/1, + bigint_to_byte_array/1, + bigint_pow/2, + bigint_gcd/2 ]). -spec wrap_i8(integer()) -> integer(). @@ -20,6 +24,10 @@ -spec wrap_u32(integer()) -> non_neg_integer(). -spec wrap_u64(integer()) -> non_neg_integer(). -spec log2(integer()) -> non_neg_integer(). +-spec bigint_from_byte_array(tuple()) -> integer(). +-spec bigint_to_byte_array(integer()) -> tuple(). +-spec bigint_pow(integer(), integer()) -> integer(). +-spec bigint_gcd(integer(), integer()) -> non_neg_integer(). %% Fixed-width (two's complement) integer semantics for .NET sized integers. %% @@ -96,3 +104,60 @@ log2_byte(N) when N >= 16#08 -> 3; log2_byte(N) when N >= 16#04 -> 2; log2_byte(N) when N >= 16#02 -> 1; log2_byte(_) -> 0. + +%% decision: encode BigInteger bytes with integer shifts -- width calculation must stay exact. +%% invariant: byte arrays use .NET's minimal little-endian two's-complement representation. +bigint_to_byte_array(0) -> + fable_utils:new_byte_array([0]); +bigint_to_byte_array(N) -> + fable_utils:new_byte_array(bigint_to_byte_list(N, [])). + +bigint_to_byte_list(N, Acc) -> + Byte = N band 16#FF, + Next = N bsr 8, + SignBitSet = Byte band 16#80 =/= 0, + case (Next =:= 0 andalso not SignBitSet) orelse (Next =:= -1 andalso SignBitSet) of + true -> lists:reverse([Byte | Acc]); + false -> bigint_to_byte_list(Next, [Byte | Acc]) + end. + +bigint_from_byte_array(BytesValue) -> + Bytes = fable_utils:byte_array_to_list(BytesValue), + case Bytes of + [] -> + 0; + _ -> + Unsigned = binary:decode_unsigned(list_to_binary(Bytes), little), + Last = lists:last(Bytes), + case Last band 16#80 of + 0 -> Unsigned; + _ -> Unsigned - (1 bsl (length(Bytes) * 8)) + end + end. + +%% decision: exponentiation stays in the integer domain -- math:pow/2 silently rounds large values. +%% invariant: bigint_pow/2 returns the exact integer result for every non-negative exponent. +bigint_pow(_Base, Exponent) when Exponent < 0 -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">> + }); +bigint_pow(Base, Exponent) -> + bigint_pow(Base, Exponent, 1). + +bigint_pow(_Base, 0, Acc) -> + Acc; +bigint_pow(Base, 1, Acc) -> + Acc * Base; +bigint_pow(Base, Exponent, Acc) when Exponent band 1 =:= 1 -> + bigint_pow(Base * Base, Exponent bsr 1, Acc * Base); +bigint_pow(Base, Exponent, Acc) -> + bigint_pow(Base * Base, Exponent bsr 1, Acc). + +bigint_gcd(A, B) -> + bigint_gcd_positive(erlang:abs(A), erlang:abs(B)). + +bigint_gcd_positive(A, 0) -> + A; +bigint_gcd_positive(A, B) -> + bigint_gcd_positive(B, A rem B). diff --git a/tests/Beam/ArithmeticTests.fs b/tests/Beam/ArithmeticTests.fs index aa6a4fe665..c88e9737b3 100644 --- a/tests/Beam/ArithmeticTests.fs +++ b/tests/Beam/ArithmeticTests.fs @@ -1095,15 +1095,53 @@ let ``test Decimal GetBits works with arithmetic operations`` () = check (2000M / 10M) check (1.5M + 0.5M) -// TODO: BigInt byte array conversion requires two's complement encoding -// which isn't natively supported in Erlang. -// [] -// let ``test Big integer to byte array works`` () = -// 32767I.ToByteArray() |> equal [|255uy; 127uy|] +[] +let ``test Big integer to byte array works`` () = + 0I.ToByteArray() |> equal [| 0uy |] + 32767I.ToByteArray() |> equal [| 255uy; 127uy |] + 32768I.ToByteArray() |> equal [| 0uy; 128uy; 0uy |] + -32768I.ToByteArray() |> equal [| 0uy; 128uy |] + -32769I.ToByteArray() |> equal [| 255uy; 127uy; 255uy |] -// [] -// let ``test Big integer from byte array works`` () = -// Numerics.BigInteger([|255uy; 127uy|]) |> equal 32767I + 111222333444555666777888999I.ToByteArray() + |> equal [| 231uy; 216uy; 2uy; 164uy; 86uy; 149uy; 8uy; 199uy; 62uy; 0uy; 92uy |] + + -111222333444555666777888999I.ToByteArray() + |> equal [| 25uy; 39uy; 253uy; 91uy; 169uy; 106uy; 247uy; 56uy; 193uy; 255uy; 163uy |] + +[] +let ``test Big integer from byte array works`` () = + Numerics.BigInteger([||]) |> equal 0I + Numerics.BigInteger([| 255uy; 127uy |]) |> equal 32767I + Numerics.BigInteger([| 0uy; 128uy; 0uy |]) |> equal 32768I + Numerics.BigInteger([| 0uy; 128uy |]) |> equal -32768I + Numerics.BigInteger([| 255uy; 127uy; 255uy |]) |> equal -32769I + + Numerics.BigInteger([| 231uy; 216uy; 2uy; 164uy; 86uy; 149uy; 8uy; 199uy; 62uy; 0uy; 92uy |]) + |> equal 111222333444555666777888999I + + Numerics.BigInteger([| 25uy; 39uy; 253uy; 91uy; 169uy; 106uy; 247uy; 56uy; 193uy; 255uy; 163uy |]) + |> equal -111222333444555666777888999I + +[] +let ``test Big integer byte arrays round-trip sign boundaries`` () = + [ -62837I; -256I; -129I; -128I; -1I; 0I; 127I; 128I; 255I; 256I ] + |> List.iter (fun value -> Numerics.BigInteger(value.ToByteArray()) |> equal value) + +[] +let ``test BigInteger Pow stays exact`` () = + bigint.Pow(0I, 0) |> equal 1I + bigint.Pow(-3I, 5) |> equal -243I + bigint.Pow(2I, 256) |> equal (1I <<< 256) + bigint.Pow(1I <<< 4_000_000, 1) |> equal (1I <<< 4_000_000) + throwsAnyError (fun () -> bigint.Pow(2I, -1) |> ignore) + +[] +let ``test BigInteger GreatestCommonDivisor works with negatives`` () = + bigint.GreatestCommonDivisor(-4I, 6I) |> equal 2I + bigint.GreatestCommonDivisor(4I, -6I) |> equal 2I + bigint.GreatestCommonDivisor(-4I, -6I) |> equal 2I + bigint.GreatestCommonDivisor(0I, -5I) |> equal 5I // --- Fixed-width integer semantics --- // .NET integers wrap on overflow; Erlang integers are arbitrary precision and never do. From de8080a34a3cca9c29c3fc7f044e13f964e892f9 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 29 Sep 2026 22:35:35 +0200 Subject: [PATCH 111/322] test(beam): restore HashSet interface parity coverage (#5012) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 1 + tests/Beam/ApplicativeTests.fs | 4 -- tests/Beam/HashSetTests.fs | 80 +++++++++++++------------ tests/Beam/MiscTests.fs | 1 - 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 20baa0898e..99346eb57a 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -475,6 +475,7 @@ suite grows. | Classes and structs | Mutable record fields, class reference equality, some self-referencing/base constructors, mutually recursive class hierarchies, and default struct construction remain incomplete. | | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | | Mutable collections | Non-byte arrays and mutable collections are process-local. List/map-backed mutation can be O(N). | +| Collection comparers | `Dictionary` and `HashSet` constructors ignore custom `IEqualityComparer` instances and use native structural keys. | | Function identity | Curry/eta identity support covers compiler-generated adapters of arity 2 through 7 and statically known function types; generic call sites can fall back to native fun identity. | | Numeric APIs | Special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | diff --git a/tests/Beam/ApplicativeTests.fs b/tests/Beam/ApplicativeTests.fs index 6f7f0f8146..430132c091 100644 --- a/tests/Beam/ApplicativeTests.fs +++ b/tests/Beam/ApplicativeTests.fs @@ -379,10 +379,6 @@ let ``test No errors because references to missing unit args`` () = let f1 = foofy "bar" f1 () |> equal "foobar" -// Skipped: Delegate generates fun(X, X) -> which requires args to be equal in Erlang -// [] -// let ``test Arity is checked also when constructing records`` () = ... - [] let ``test Aether with generics works`` () = // See #750 let a = { RecordB = {A= "foo"; B=true} } diff --git a/tests/Beam/HashSetTests.fs b/tests/Beam/HashSetTests.fs index 3782dca90b..306a29e2bc 100644 --- a/tests/Beam/HashSetTests.fs +++ b/tests/Beam/HashSetTests.fs @@ -391,32 +391,34 @@ let ``test HashSet.CopyTo works`` () = arr3[3..] |> Array.distinct |> Array.length |> equal 4 arr3.Length |> equal 7 -// [] -// let ``test HashSet IReadOnlyCollection.Count works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> IReadOnlyCollection<_> -// coll.Count |> equal 3 - -// [] -// let ``test HashSet ICollection.IsReadOnly works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.IsReadOnly |> equal false +[] +let ``test HashSet IReadOnlyCollection.Count works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> IReadOnlyCollection<_> + coll.Count |> equal 3 -// [] -// let ``test HashSet ICollection.Count works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Count |> equal 3 +[] +let ``test HashSet ICollection.IsReadOnly works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.IsReadOnly |> equal false -// [] -// let ``test HashSet ICollection.Contains works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Contains(("B", 3)) |> equal false -// coll.Contains(("D", 3)) |> equal false -// coll.Contains(("B", 2)) |> equal true +[] +let ``test HashSet ICollection.Count works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Count |> equal 3 +[] +let ``test HashSet ICollection.Contains works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Contains(("B", 3)) |> equal false + coll.Contains(("D", 3)) |> equal false + coll.Contains(("B", 2)) |> equal true + +// TODO: Interface-typed collection calls do not retain the concrete HashSet kind, +// so ICollection.CopyTo cannot select fable_hashset:copy_to yet. // [] // let ``test HashSet ICollection.CopyTo works`` () = // let xs = [| ("A", 1); ("B", 2); ("C", 3) |] @@ -425,13 +427,15 @@ let ``test HashSet.CopyTo works`` () = // coll.CopyTo(ys, 0) // ys = xs |> equal true -// [] -// let ``test HashSet ICollection.Clear works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Clear() -// coll.Count |> equal 0 - +[] +let ``test HashSet ICollection.Clear works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Clear() + coll.Count |> equal 0 + +// TODO: Interface-typed collection calls do not retain the concrete HashSet kind, +// so ICollection.Add cannot select fable_hashset:add yet. // [] // let ``test HashSet ICollection.Add works`` () = // let xs = [| ("A", 1); ("B", 2); ("C", 3) |] @@ -441,11 +445,11 @@ let ``test HashSet.CopyTo works`` () = // coll.Add(("D", 4)) // coll.Count |> equal 5 -// [] -// let ``test HashSet ICollection.Remove works`` () = -// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] -// let coll = (HashSet xs) :> ICollection<_> -// coll.Remove(("B", 3)) |> equal false -// coll.Remove(("D", 3)) |> equal false -// coll.Remove(("B", 2)) |> equal true -// coll.Count |> equal 2 +[] +let ``test HashSet ICollection.Remove works`` () = + let xs = [| ("A", 1); ("B", 2); ("C", 3) |] + let coll = (HashSet xs) :> ICollection<_> + coll.Remove(("B", 3)) |> equal false + coll.Remove(("D", 3)) |> equal false + coll.Remove(("B", 2)) |> equal true + coll.Count |> equal 2 diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index 10e63de361..ffe0e9a08b 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -357,7 +357,6 @@ let ``test Custom computation expressions work`` () = execMaybe 99 |> equal None // -- Self References in Constructors -- -// TODO: "as myself" self-reference in constructors generates {badkey,x} — class constructor codegen issue [] let ``test Self references in constructors work`` () = From 63a6f12aa742e720d3040423c19707d77092e8bd Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 29 Sep 2026 22:58:23 +0200 Subject: [PATCH 112/322] fix(beam): preserve array identity hashing (#5014) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 4 +-- src/Fable.Transforms/Beam/Replacements.fs | 2 +- src/fable-library-beam/fable_comparison.erl | 8 +++++- tests/Beam/ComparisonTests.fs | 27 ++++++++++++++++----- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 99346eb57a..6a3958cf31 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -299,7 +299,8 @@ Unsigned values keep their unsigned range, so `UInt64.MaxValue` is represented a - Native `=:=` supplies deep equality for numbers, atoms, binaries, tuples, lists, and maps. -- `erlang:phash2/1` supplies general hashing. +- `fable_comparison:hash/1` supplies structural hashing through `erlang:phash2/1`; + array `GetHashCode()` hashes the storage handle to preserve identity across mutation. - Direct comparison of a statically known union uses `fable_comparison:compare_union/3` with case declaration order. - Direct union relational operators, `compare`, `min`/`max`, and plain @@ -480,7 +481,6 @@ suite grows. | Numeric APIs | Special floating-point values need parity work. | | Formatting APIs | `FormattableString`, some custom `TimeSpan` formats, and width-sensitive negative hexadecimal formatting are incomplete. | | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | -| Hashing | Array hashing currently follows content rather than .NET reference identity. | | Recursive values | Recursive value bindings that lower through `Lazy` and some inline module-value side effects are incomplete. | | Cancellation | Cross-process callbacks cannot safely mutate captured process-local values. Callback exceptions are suppressed, and `CancellationTokenSource.Dispose()` remains a no-op. | | Diagnostics | Unhandled Fable value kinds and unsupported assignment shapes can still compile to runtime `erlang:error({unsupported_*})` paths instead of failing compilation. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index ead36e8f0b..733f53ed6d 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -796,7 +796,7 @@ let private objects | _, Array _ -> physicalEquals r arg1 arg2 |> Some | _ -> equals com r true arg1 arg2 |> Some | "GetHashCode", Some thisObj, [] -> - Helper.LibCall(com, "fable_comparison", "hash", t, [ thisObj ], ?loc = r) + Helper.LibCall(com, "fable_comparison", "get_hash_code", t, [ thisObj ], ?loc = r) |> Some | "GetType", Some arg, _ -> makeTypeInfo r arg.Type |> Some | "ToString", Some thisObj, [] -> ToString.toStringByType com r t thisObj diff --git a/src/fable-library-beam/fable_comparison.erl b/src/fable-library-beam/fable_comparison.erl index ad71b43944..5897f152f4 100644 --- a/src/fable-library-beam/fable_comparison.erl +++ b/src/fable-library-beam/fable_comparison.erl @@ -1,9 +1,10 @@ -module(fable_comparison). --export([compare/2, compare_union/3, equals/2, hash/1]). +-export([compare/2, compare_union/3, equals/2, get_hash_code/1, hash/1]). -spec equals(term(), term()) -> boolean(). -spec compare(term(), term()) -> -1 | 0 | 1. -spec compare_union([binary()], term(), term()) -> -1 | 0 | 1. +-spec get_hash_code(term()) -> non_neg_integer(). -spec hash(term()) -> non_neg_integer(). %% Deep equality that handles ref-wrapped arrays (process dict refs) @@ -152,6 +153,11 @@ union_tag_index([Name | _], Name, I) -> I; union_tag_index([_ | T], Name, I) -> union_tag_index(T, Name, I + 1); union_tag_index([], _, _) -> -1. +%% invariant: Object.GetHashCode hashes reference-backed storage by identity, while hash/1 remains structural. +get_hash_code({byte_array, _, _} = V) -> erlang:phash2(V); +get_hash_code(V) when is_reference(V) -> erlang:phash2(V); +get_hash_code(V) -> hash(V). + %% Hash that derefs refs and byte arrays before hashing. hash({byte_array, _, _} = V) -> erlang:phash2(fable_utils:byte_array_to_list(V)); diff --git a/tests/Beam/ComparisonTests.fs b/tests/Beam/ComparisonTests.fs index 1754c9f926..4f24be8278 100644 --- a/tests/Beam/ComparisonTests.fs +++ b/tests/Beam/ComparisonTests.fs @@ -23,6 +23,8 @@ let fableCompare (a: obj) (b: obj) : int = nativeOnly let fableHash (a: obj) : int = nativeOnly #endif +let private getObjectHashCode (value: obj) = value.GetHashCode() + type UTest = A of int | B of int type RTest = { a: int; b: int } @@ -671,12 +673,25 @@ let ``test GetHashCode with records works`` () = ({a=1; b=2}.GetHashCode(), {a=1; b=2}.GetHashCode()) ||> equal ({a=2; b=1}.GetHashCode(), {a=1; b=2}.GetHashCode()) ||> notEqual -// TODO: GetHashCode on arrays uses identity (ref) hashing on .NET, content hashing on Beam. -// Test fails on .NET because two [|1; 2|] literals are different instances with different hash codes. -// [] -// let ``test GetHashCode with arrays works`` () = -// ([|1; 2|].GetHashCode(), [|1; 2|].GetHashCode()) ||> equal -// ([|2; 1|].GetHashCode(), [|1; 2|].GetHashCode()) ||> notEqual +[] +let ``test GetHashCode with arrays uses identity`` () = + let xs1 = [| 1; 2 |] + let xs2 = [| 1; 2 |] + let hash1 = getObjectHashCode xs1 + (hash1, xs1.GetHashCode()) ||> equal + (hash1, getObjectHashCode xs2) ||> notEqual + xs1[0] <- 2 + (hash1, getObjectHashCode xs1) ||> equal + +[] +let ``test GetHashCode with byte arrays uses identity`` () = + let xs1 = [| 1uy; 2uy |] + let xs2 = [| 1uy; 2uy |] + let hash1 = getObjectHashCode xs1 + (hash1, xs1.GetHashCode()) ||> equal + (hash1, getObjectHashCode xs2) ||> notEqual + xs1[0] <- 2uy + (hash1, getObjectHashCode xs1) ||> equal [] let ``test Set option equality works`` () = From 7b3d762debe2b5ab4a15e27a83b211474a7ce3af Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Tue, 29 Sep 2026 23:17:07 +0200 Subject: [PATCH 113/322] fix(beam): eliminate unsupported expression placeholders (#5013) --- src/Fable.Build/Test/Beam.fs | 69 ++++++++++- src/Fable.Transforms/Beam/FABLE-BEAM.md | 1 - src/Fable.Transforms/Beam/Fable2Beam.fs | 49 ++++---- src/Fable.Transforms/Beam/Replacements.fs | 117 ++++++++++++++---- tests/Beam/ArrayTests.fs | 29 +++++ .../Beam/UnsupportedNumericLiteral/Program.fs | 3 + .../UnsupportedNumericLiteral.fsproj | 13 ++ 7 files changed, 225 insertions(+), 56 deletions(-) create mode 100644 tests/Beam/UnsupportedNumericLiteral/Program.fs create mode 100644 tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj diff --git a/src/Fable.Build/Test/Beam.fs b/src/Fable.Build/Test/Beam.fs index 13709cb2a1..58e82044a4 100644 --- a/src/Fable.Build/Test/Beam.fs +++ b/src/Fable.Build/Test/Beam.fs @@ -60,17 +60,76 @@ let private runErl (workingDir: string) (paArgs: string) (expr: string) = // stderr is folded into the returned text so a crash report reaches the failure message. stdout.Result + stderr.Result, proc.ExitCode +/// Compile a fixture with the local Fable CLI while retaining diagnostics and the non-zero exit code. +let private runFable (workingDir: string) (args: string list) = + let startInfo = + System.Diagnostics.ProcessStartInfo( + FileName = "dotnet", + WorkingDirectory = workingDir, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = System.Text.Encoding.UTF8, + StandardErrorEncoding = System.Text.Encoding.UTF8 + ) + + let localFableDir = Path.Resolve("src", "Fable.Cli") + + for arg in [ "run"; "-c"; "Release"; "--project"; localFableDir; "--" ] @ args do + startInfo.ArgumentList.Add(arg) + + use proc = System.Diagnostics.Process.Start(startInfo) + let stdout = proc.StandardOutput.ReadToEndAsync() + let stderr = proc.StandardError.ReadToEndAsync() + proc.WaitForExit() + stdout.Result + stderr.Result, proc.ExitCode + let private expect (what: string) (expected: 'a) (actual: 'a) = if expected <> actual then - failwith $"Entry point test: %s{what}\n expected: %A{expected}\n actual: %A{actual}" + failwith $"BEAM test: %s{what}\n expected: %A{expected}\n actual: %A{actual}" /// Assert that a program's output contains `substring`, showing the output when it does not. /// `expect` on a bare `output.Contains ...` reports only `expected: true / actual: false`, which /// leaves a mismatch undiagnosable without re-running the program by hand. let private expectContains (what: string) (substring: string) (output: string) = if not (output.Contains substring) then - failwith - $"Entry point test: %s{what}\n expected output to contain: %s{substring}\n actual output:\n%s{output}" + failwith $"BEAM test: %s{what}\n expected output to contain: %s{substring}\n actual output:\n%s{output}" + +let private testUnsupportedDiagnostics () = + let programSourceDir = Path.Resolve("tests", "Beam", "UnsupportedNumericLiteral") + let programBuildDir = Path.Resolve("temp", "tests", "BeamUnsupportedNumericLiteral") + Directory.clean programBuildDir + + let output, exitCode = + runFable + programBuildDir + [ + programSourceDir + "--outDir" + programBuildDir + "--lang" + "beam" + "--exclude" + "Fable.Core" + "--noCache" + ] + + expect "unsupported numeric literal compilation fails" 1 exitCode + + expectContains + "unsupported numeric literal has an actionable source range" + "Program.fs(3,12): (3,31) error FABLE: Numeric literal kind 'Float16' is not supported for Beam target" + output + + let generatedFile = + Path.Combine(programBuildDir, "src", "unsupported_numeric_literal_program.erl") + + let generatedCode = File.ReadAllText(generatedFile) + + expect + "unsupported numeric literal does not emit a runtime error placeholder" + false + (generatedCode.Contains("erlang:error(unsupported_")) /// Compile a whole program and run it on the BEAM through the generated `main.erl` shim. /// @@ -233,3 +292,7 @@ let handle (args: string list) = // The suite above calls test functions directly and never runs a program end to end, so the // generated entry point shim is only exercised here. testEntryPointPrograms () + + // Unsupported constructs must stop compilation at their source location rather than leave + // an erlang:error placeholder that fails only when the generated code runs. + testUnsupportedDiagnostics () diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 6a3958cf31..18ae1cc8e4 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -483,7 +483,6 @@ suite grows. | Defaults and null | `Unchecked.defaultof` and null semantics differ for strings, structs, and erased values. | | Recursive values | Recursive value bindings that lower through `Lazy` and some inline module-value side effects are incomplete. | | Cancellation | Cross-process callbacks cannot safely mutate captured process-local values. Callback exceptions are suppressed, and `CancellationTokenSource.Dispose()` remains a no-op. | -| Diagnostics | Unhandled Fable value kinds and unsupported assignment shapes can still compile to runtime `erlang:error({unsupported_*})` paths instead of failing compilation. | For `char` conversion in generic code, making the function `inline` or using a concrete `char` annotation keeps the type available at the call site. Other entries diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 553bd13cb8..1b18f2be4f 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -134,6 +134,12 @@ let private hasMutableRecordFields (com: IBeamCompiler) (typ: Fable.AST.Fable.Ty let private atomLit name = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom name)) +let private unsupportedExpression (com: IBeamCompiler) range message = + com.AddLog(message, Severity.Error, ?range = range, fileName = com.CurrentFile, tag = "FABLE") + + // invariant: Unsupported source constructs log an error before returning a recovery expression. + atomLit "undefined" + /// Resolve the atom tag name for a union case. /// Returns Some(atomStr, isFieldless) or None if the entity can't be resolved. let private getUnionCaseAtomExpr (com: IBeamCompiler) (ref: EntityRef) (tag: int) = @@ -239,7 +245,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "undefined")) - | Value(kind, _range) -> transformValue com ctx kind + | Value(kind, range) -> transformValue com ctx range kind | Call(callee, info, _typ, _range) -> transformCall com ctx callee info @@ -580,7 +586,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er transformExpr com ctx target - | Set(expr, ValueSet, _typ, value, _range) -> + | Set(expr, ValueSet, _typ, value, range) -> match expr with | IdentExpr ident when ctx.MutableVars.ContainsKey(ident.Name) -> // Mutable variable: update via process dictionary using unique ref key @@ -601,14 +607,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er ] ) | IdentExpr ident -> Beam.ErlExpr.Match(Beam.PVar(capitalizeFirst ident.Name), transformExpr com ctx value) - | _ -> - com.WarnOnlyOnce("Set with non-identifier target is not supported for Beam target") - - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "unsupported_set")) ] - ) + | _ -> unsupportedExpression com range "Set with non-identifier target is not supported for Beam target" | Set(expr, FieldSet fieldName, _, value, _) -> let erlExpr = transformExpr com ctx expr @@ -1453,7 +1452,13 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er } ] -and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam.ErlExpr = +and transformValue + (com: IBeamCompiler) + (ctx: Context) + (range: SourceLocation option) + (value: ValueKind) + : Beam.ErlExpr + = match value with | StringConstant s -> Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit s) @@ -1619,6 +1624,13 @@ and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam Beam.ErlExpr.Literal(Beam.ErlLiteral.BigInt(string value)) + | NumberConstant(NumberValue.Float16 _, _) -> + unsupportedExpression com range "Numeric literal kind 'Float16' is not supported for Beam target" + | NumberConstant(NumberValue.Int128 _, _) -> + unsupportedExpression com range "Numeric literal kind 'Int128' is not supported for Beam target" + | NumberConstant(NumberValue.UInt128 _, _) -> + unsupportedExpression com range "Numeric literal kind 'UInt128' is not supported for Beam target" + | NewRecord(values, ref, _genArgs) -> match com.TryGetEntity(ref) with | Some entity -> @@ -1763,21 +1775,6 @@ and transformValue (com: IBeamCompiler) (ctx: Context) (value: ValueKind) : Beam let flagsExpr = Beam.ErlExpr.Literal(Beam.ErlLiteral.Integer(int64 flagBits)) Beam.ErlExpr.Call(Some "fable_regex", "create", [ patternExpr; flagsExpr ]) - | _ -> - let kindName = value.GetType().Name - - com.WarnOnlyOnce( - $"Unhandled Fable value kind '%s{kindName}' — emitting placeholder atom. This may cause runtime errors." - ) - - Beam.ErlExpr.Call( - Some "erlang", - "error", - [ - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom $"unsupported_%s{kindName.ToLowerInvariant()}")) - ] - ) - and transformOperation (com: IBeamCompiler) (ctx: Context) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 733f53ed6d..241d19792d 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -206,6 +206,31 @@ let private asArrayRef (com: ICompiler) r (t: Type) (expr: Expr) = | Array _ -> wrapArr com r t expr | _ -> Helper.LibCall(com, "fable_utils", "new_ref", t, [ expr ], ?loc = r) +let private captureExprOnce (com: ICompiler) name (expr: Expr) build = + let ident = makeTypedIdent expr.Type $"%s{name}_%d{com.IncrementCounter()}" + + // invariant: A captured expression is evaluated once before every use of its generated identifier. + Let(ident, expr, build (IdentExpr ident)) + +let private captureExprs2Once com (name1, expr1) (name2, expr2) build = + captureExprOnce com name1 expr1 (fun expr1 -> captureExprOnce com name2 expr2 (build expr1)) + +let private captureExprs4Once com arg1 arg2 arg3 arg4 build = + captureExprs2Once com arg1 arg2 (fun expr1 expr2 -> captureExprs2Once com arg3 arg4 (build expr1 expr2)) + +let private captureExprs5Once com arg1 arg2 arg3 arg4 arg5 build = + captureExprs2Once + com + arg1 + arg2 + (fun expr1 expr2 -> + captureExprs2Once + com + arg3 + arg4 + (fun expr3 expr4 -> captureExprOnce com (fst arg5) (snd arg5) (build expr1 expr2 expr3 expr4)) + ) + let private getOne (com: ICompiler) (ctx: Context) (t: Type) = match t with | Boolean -> makeBoolConst true @@ -766,7 +791,7 @@ let private unchecked match typ with | Boolean -> makeBoolConst false |> Some - | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue None |> Some + | Number(kind, uom) -> NumberConstant(NumberValue.GetZero kind, uom) |> makeValue r |> Some | Char -> CharConstant '\u0000' |> makeValue None |> Some | String -> makeStrConst "" |> Some | _ -> Value(Null typ, r) |> Some @@ -2939,38 +2964,78 @@ let private arrayModule | "ToSeq", [ arr ] -> derefArr r arr |> Some // === In-place mutation: deref for computation, put result back === | "SortInPlace", [ arr ] -> - let derefed = derefArr r arr - let sorted = emitExpr r t [ derefed ] "lists:sort($0)" - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + captureExprOnce + com + "array" + arr + (fun arr -> + let derefed = derefArr r arr + let sorted = emitExpr r t [ derefed ] "lists:sort($0)" + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "SortInPlaceBy", [ fn; arr ] -> - let derefed = derefArr r arr - let sorted = Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + captureExprs2Once + com + ("projection", fn) + ("array", arr) + (fun fn arr -> + let derefed = derefArr r arr + let sorted = Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "SortInPlaceWith", [ fn; arr ] -> - let derefed = derefArr r arr - let sorted = Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; derefed ]) - Fable.Set(arr, ValueSet, Type.Unit, sorted, None) |> Some + captureExprs2Once + com + ("comparer", fn) + ("array", arr) + (fun fn arr -> + let derefed = derefArr r arr + let sorted = Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; derefed ]) + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) + ) + |> Some | "Fill", [ arr; start; count; value ] -> - let derefed = derefArr r arr + captureExprs4Once + com + ("array", arr) + ("start", start) + ("count", count) + ("value", value) + (fun arr start count value -> + let derefed = derefArr r arr - let filled = - Helper.LibCall(com, "fable_resize_array", "fill", t, [ derefed; start; count; value ]) + let filled = + Helper.LibCall(com, "fable_resize_array", "fill", t, [ derefed; start; count; value ]) - Fable.Set(arr, ValueSet, Type.Unit, filled, None) |> Some + Fable.Set(arr, ValueSet, Type.Unit, filled, None) + ) + |> Some | "CopyTo", [ source; sourceIdx; target; targetIdx; count ] -> - let derefSource = derefArr r source - let derefTarget = derefArr r target - - let blitted = - Helper.LibCall( - com, - "fable_resize_array", - "blit", - t, - [ derefSource; sourceIdx; derefTarget; targetIdx; count ] + captureExprs5Once + com + ("source", source) + ("source_index", sourceIdx) + ("target", target) + ("target_index", targetIdx) + ("count", count) + (fun source sourceIdx target targetIdx count -> + let derefSource = derefArr r source + let derefTarget = derefArr r target + + let blitted = + Helper.LibCall( + com, + "fable_resize_array", + "blit", + t, + [ derefSource; sourceIdx; derefTarget; targetIdx; count ] + ) + + Fable.Set(target, ValueSet, Type.Unit, blitted, None) ) - - Fable.Set(target, ValueSet, Type.Unit, blitted, None) |> Some + |> Some | _ -> None /// Beam-specific Array.Parallel module replacements — dispatches to fable_parallel. diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 5369af0da2..9b4544b7d5 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -1078,6 +1078,35 @@ let ``test Array.sortInPlaceWith works`` () = Array.sortInPlaceWith (fun x y -> int(x - y)) xs xs.[0] + xs.[1] |> equal 3. +[] +let ``test In-place array functions evaluate computed arrays once`` () = + let mutable evaluations = 0 + let computed xs () = + evaluations <- evaluations + 1 + xs + + let sorted = [| 3; 1; 2 |] + Array.sortInPlace (computed sorted ()) + sorted |> equal [| 1; 2; 3 |] + + let sortedBy = [| 3; 1; 2 |] + Array.sortInPlaceBy (fun x -> -x) (computed sortedBy ()) + sortedBy |> equal [| 3; 2; 1 |] + + let sortedWith = [| 3; 1; 2 |] + Array.sortInPlaceWith compare (computed sortedWith ()) + sortedWith |> equal [| 1; 2; 3 |] + + let filled = [| 1; 2; 3; 4 |] + Array.fill (computed filled ()) 1 2 9 + filled |> equal [| 1; 9; 9; 4 |] + + let source = [| 5; 6; 7 |] + let target = [| 0; 0; 0 |] + Array.blit (computed source ()) 1 (computed target ()) 0 2 + target |> equal [| 6; 7; 0 |] + evaluations |> equal 6 + [] let ``test Array.sortByDescending works`` () = let xs = [|3.; 4.; 1.; 2.|] diff --git a/tests/Beam/UnsupportedNumericLiteral/Program.fs b/tests/Beam/UnsupportedNumericLiteral/Program.fs new file mode 100644 index 0000000000..ce52d638be --- /dev/null +++ b/tests/Beam/UnsupportedNumericLiteral/Program.fs @@ -0,0 +1,3 @@ +module Fable.Tests.UnsupportedNumericLiteral + +let value = Unchecked.defaultof diff --git a/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj b/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj new file mode 100644 index 0000000000..705031e61e --- /dev/null +++ b/tests/Beam/UnsupportedNumericLiteral/UnsupportedNumericLiteral.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + false + preview + + + + + + + + From 78c3b55f52581f9889cbca6bd2c51e260c35e1a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:35:10 +0200 Subject: [PATCH 114/322] chore: Bump markdown-it from 14.3.0 to 14.3.2 (#5015) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c0940a2504..90a8072726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1545,9 +1545,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", - "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", "dev": true, "funding": [ { From 67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 00:13:13 +0200 Subject: [PATCH 115/322] fix(beam): preserve union collection ordering (#5016) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 9 +- src/Fable.Transforms/Beam/Replacements.fs | 121 ++++++++++++++++++---- src/fable-library-beam/fable_list.erl | 64 ++++++++++++ tests/Beam/UnionTypeTests.fs | 112 ++++++++++++++++++++ 4 files changed, 280 insertions(+), 26 deletions(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 18ae1cc8e4..39ffd753d2 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -303,8 +303,9 @@ Unsigned values keep their unsigned range, so `UInt64.MaxValue` is represented a array `GetHashCode()` hashes the storage handle to preserve identity across mutation. - Direct comparison of a statically known union uses `fable_comparison:compare_union/3` with case declaration order. -- Direct union relational operators, `compare`, `min`/`max`, and plain - `List.sort`/`Array.sort` use the same declaration-order comparison. +- Direct union relational operators, `compare`, collection `min`/`max`, and + `List`/`Array` sorting operations use the same declaration-order comparison + when the union type is statically known. - Function physical equality unwraps compiler-generated curry/eta adapters before comparing the underlying fun. @@ -467,7 +468,7 @@ suite grows. | Area | Current behavior | | --- | --- | -| Union ordering | Generic comparison, union-valued `sortBy` keys, collection `min`/`max`, nested union fields, and union keys/elements in `Map`/`Set` can use Erlang atom order instead of declaration order. | +| Union ordering | Generic comparison, nested union fields, and union keys/elements in `Map`/`Set` can use Erlang atom order instead of declaration order. | | Options | Erasure can still conflate `None`, `Some null`/`Some undefined`, and some nested option paths after static type information is lost. | | `char` | A generic or `obj`-erased character is an integer at runtime, so `string` and `%A` can print its codepoint. UTF-16 surrogate behavior is not complete. | | Structured formatting | `%A` reconstructs values from term shape. Record field order, original names, erased options, sets, chars, refs/arrays, decimals, and date/time values can differ from .NET output. | @@ -510,7 +511,7 @@ visible rather than silent correctness failures. | Priority | Gap | Suggested direction | | --- | --- | --- | | P0 | Unsupported AST paths can survive compilation | Do not silently emit runtime placeholders for constructs inside the supported surface. Prefer compiler errors with source ranges for statically detectable unsupported constructs. | -| P0 | Union declaration ordering is incomplete | Thread union-aware comparers through `sortBy`, `min`/`max`, nested comparison, and ordered collections. If type-directed routing cannot cover generic containers, define a versioned DU/collection representation change. | +| P0 | Union declaration ordering is incomplete | Thread union-aware comparers through nested comparison and ordered collections. If type-directed routing cannot cover generic containers, define a versioned DU/collection representation change. | | P0 | Option erasure loses states in generic and null-like paths | Carry the nested-option decision through replacements and collection helpers, or adopt an unambiguous tagged form where erasure is unsafe. | | P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | | P0 | Object-model gaps affect valid F# | Fix silent wrong-code paths in the claimed object-model surface. Keep unsupported class and struct forms as explicit exclusions until implemented. | diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 241d19792d..c88a91dcb1 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -174,6 +174,31 @@ let private compare (com: ICompiler) r (left: Expr) (right: Expr) = Helper.LibCall(com, modName, "compare", Number(Int32, NumberInfo.Empty), [ left; right ], ?loc = r) +let private makeComparerFunction (com: ICompiler) ctx typArg = + let x = makeUniqueIdent com ctx typArg "x" + let y = makeUniqueIdent com ctx typArg "y" + let body = compare com None (IdentExpr x) (IdentExpr y) + Delegate([ x; y ], body, None, Tags.empty) + +let private tryUnionComparerFunction (com: ICompiler) ctx typ = + // decision: Keep native ordering for ordinary values and inject declaration ordering only + // when the collection operation retains a statically known union type. + if isFableUnion com typ then + makeComparerFunction com ctx typ |> Some + else + None + +let private tryGenericUnionComparerFunction (com: ICompiler) ctx index (info: CallInfo) = + info.GenericArgs + |> List.tryItem index + |> Option.bind (tryUnionComparerFunction com ctx) + +let private tryCollectionUnionComparerFunction (com: ICompiler) ctx typ = + match typ with + | List elemType + | Array(elemType, _) -> tryUnionComparerFunction com ctx elemType + | _ -> None + /// Relational operator (`<`, `<=`, `>`, `>=`): native Erlang term ordering for most /// types, but structural `compare_union` ordering for F# unions and instant ordering /// for dates. @@ -1712,7 +1737,7 @@ let private convert /// Lists in Erlang are native linked lists, same as F#. let private listModule (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) @@ -1751,7 +1776,10 @@ let private listModule "lists:sort(fun(A, B) -> fable_comparison:compare_union($0, A, B) =< 0 end, $1)" |> Some | None -> emitExpr r t [ list ] "lists:sort($0)" |> Some - | "SortBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; list ]) |> Some + | "SortBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; list ]) |> Some | "SortDescending", [ list ] -> match (match list.Type with @@ -1767,7 +1795,11 @@ let private listModule |> Some | None -> emitExpr r t [ list ] "lists:reverse(lists:sort($0))" |> Some | "SortByDescending", [ fn; list ] -> - Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; list ]) |> Some + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> + Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; comparer; list ]) + |> Some + | None -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; list ]) |> Some | "SortWith", [ fn; list ] -> Helper.LibCall(com, "fable_list", "sort_with", t, [ fn; list ]) |> Some | "Contains", [ item; list ] -> emitExpr r t [ item; list ] "lists:member($0, $1)" |> Some | "Exists", [ fn; list ] -> emitExpr r t [ fn; list ] "lists:any($0, $1)" |> Some @@ -1780,10 +1812,22 @@ let private listModule | "Partition", [ fn; list ] -> emitExpr r t [ fn; list ] "lists:partition($0, $1)" |> Some | "Zip", [ l1; l2 ] -> Helper.LibCall(com, "fable_list", "zip", t, [ l1; l2 ]) |> Some | "Unzip", [ list ] -> emitExpr r t [ list ] "lists:unzip($0)" |> Some - | "Min", [ list ] -> emitExpr r t [ list ] "lists:min($0)" |> Some - | "Max", [ list ] -> emitExpr r t [ list ] "lists:max($0)" |> Some - | "MinBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; list ]) |> Some - | "MaxBy", [ fn; list ] -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; list ]) |> Some + | "Min", [ list ] -> + match tryCollectionUnionComparerFunction com ctx list.Type with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_with", t, [ comparer; list ]) |> Some + | None -> emitExpr r t [ list ] "lists:min($0)" |> Some + | "Max", [ list ] -> + match tryCollectionUnionComparerFunction com ctx list.Type with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_with", t, [ comparer; list ]) |> Some + | None -> emitExpr r t [ list ] "lists:max($0)" |> Some + | "MinBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; list ]) |> Some + | "MaxBy", [ fn; list ] -> + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; comparer; list ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; list ]) |> Some | "Indexed", [ list ] -> Helper.LibCall(com, "fable_list", "indexed", t, [ list ]) |> Some | "ReduceBack", [ fn; list ] -> Helper.LibCall(com, "fable_list", "reduce_back", t, [ fn; list ]) |> Some | ("Init" | "Initialize"), [ count; fn ] -> Helper.LibCall(com, "fable_list", "init", t, [ count; fn ]) |> Some @@ -2090,12 +2134,6 @@ let private stringModule | "Filter", [ fn; str ] -> Helper.LibCall(com, "fable_string", "filter", t, [ fn; str ]) |> Some | _ -> None -let private makeComparerFunction (com: ICompiler) ctx typArg = - let x = makeUniqueIdent com ctx typArg "x" - let y = makeUniqueIdent com ctx typArg "y" - let body = compare com None (IdentExpr x) (IdentExpr y) - Delegate([ x; y ], body, None, Tags.empty) - let private makeComparer (com: ICompiler) ctx typArg = objExpr [ "Compare", makeComparerFunction com ctx typArg ] @@ -2358,7 +2396,7 @@ let private arrays /// Beam-specific Array module replacements. let private arrayModule (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) @@ -2376,8 +2414,22 @@ let private arrayModule | "Tail", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "erlang:tl($0)" |> wrapArr com r t |> Some | "IsEmpty", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "($0 =:= [])" |> Some | "Sum", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:sum($0)" |> Some - | "Min", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:min($0)" |> Some - | "Max", [ arr ] -> let arr = derefArr r arr in emitExpr r t [ arr ] "lists:max($0)" |> Some + | "Min", [ arr ] -> + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + + let arr = derefArr r arr + + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_with", t, [ comparer; arr ]) |> Some + | None -> emitExpr r t [ arr ] "lists:min($0)" |> Some + | "Max", [ arr ] -> + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + + let arr = derefArr r arr + + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_with", t, [ comparer; arr ]) |> Some + | None -> emitExpr r t [ arr ] "lists:max($0)" |> Some | "Contains", [ value; arr ] -> let arr = derefArr r arr in emitExpr r t [ value; arr ] "lists:member($0, $1)" |> Some | "Exists", [ fn; arr ] -> let arr = derefArr r arr in emitExpr r t [ fn; arr ] "lists:any($0, $1)" |> Some @@ -2397,10 +2449,16 @@ let private arrayModule Helper.LibCall(com, "fable_list", "sum_by", t, [ fn; arr ]) |> Some | "MinBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "min_by", t, [ fn; arr ]) |> Some + + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; comparer; arr ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "min_by", t, [ fn; arr ]) |> Some | "MaxBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "max_by", t, [ fn; arr ]) |> Some + + match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; comparer; arr ]) |> Some + | None -> Helper.LibCall(com, "fable_list", "max_by", t, [ fn; arr ]) |> Some | "Find", [ fn; arr ] -> let arr = derefArr r arr Helper.LibCall(com, "fable_list", "find", t, [ fn; arr ]) |> Some @@ -2623,7 +2681,9 @@ let private arrayModule | "SortBy", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; arr ]) + (match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; arr ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; arr ])) |> wrapArr com r t |> Some | "SortWith", [ fn; arr ] -> @@ -2635,7 +2695,9 @@ let private arrayModule | "SortByDescending", [ fn; arr ] -> let arr = derefArr r arr - Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; arr ]) + (match tryGenericUnionComparerFunction com ctx 1 info with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; comparer; arr ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by_descending", t, [ fn; arr ])) |> wrapArr com r t |> Some | "Zip", [ arr1; arr2 ] -> @@ -2964,24 +3026,39 @@ let private arrayModule | "ToSeq", [ arr ] -> derefArr r arr |> Some // === In-place mutation: deref for computation, put result back === | "SortInPlace", [ arr ] -> + let comparer = tryCollectionUnionComparerFunction com ctx arr.Type + captureExprOnce com "array" arr (fun arr -> let derefed = derefArr r arr - let sorted = emitExpr r t [ derefed ] "lists:sort($0)" + + let sorted = + match comparer with + | Some comparer -> + emitExpr r t [ comparer; derefed ] "lists:sort(fun(A, B) -> $0(A, B) =< 0 end, $1)" + | None -> emitExpr r t [ derefed ] "lists:sort($0)" + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) ) |> Some | "SortInPlaceBy", [ fn; arr ] -> + let comparer = tryGenericUnionComparerFunction com ctx 1 info + captureExprs2Once com ("projection", fn) ("array", arr) (fun fn arr -> let derefed = derefArr r arr - let sorted = Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) + + let sorted = + match comparer with + | Some comparer -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; comparer; derefed ]) + | None -> Helper.LibCall(com, "fable_list", "sort_by", t, [ fn; derefed ]) + Fable.Set(arr, ValueSet, Type.Unit, sorted, None) ) |> Some diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 948865dd99..15c526e5f8 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -9,7 +9,9 @@ reduce_back/2, map_indexed/2, sort_by/2, + sort_by/3, sort_by_descending/2, + sort_by_descending/3, sort_with/2, find/2, try_find/2, @@ -23,7 +25,11 @@ collect/2, sum_by/2, min_by/2, + min_by/3, max_by/2, + max_by/3, + min_with/2, + max_with/2, indexed/1, zip/2, zip3/3, @@ -207,9 +213,15 @@ map_indexed(Fn, List) -> sort_by(Fn, List) -> lists:sort(fun(A, B) -> Fn(A) =< Fn(B) end, List). +sort_by(Fn, Comparer, List) -> + lists:sort(fun(A, B) -> Comparer(Fn(A), Fn(B)) =< 0 end, List). + sort_by_descending(Fn, List) -> lists:reverse(lists:sort(fun(A, B) -> Fn(A) =< Fn(B) end, List)). +sort_by_descending(Fn, Comparer, List) -> + lists:reverse(lists:sort(fun(A, B) -> Comparer(Fn(A), Fn(B)) =< 0 end, List)). + sort_with(Fn, List) -> lists:sort(fun(A, B) -> (Fn(A))(B) =< 0 end, List). @@ -271,9 +283,61 @@ sum_by(Fn, List) -> min_by(Fn, List) -> element(2, lists:min(lists:map(fun(E) -> {Fn(E), E} end, List))). +min_by(Fn, Comparer, [Head | Tail]) -> + {_, Result} = lists:foldl( + fun(Item, {BestKey, Best}) -> + ItemKey = Fn(Item), + case Comparer(ItemKey, BestKey) < 0 of + true -> {ItemKey, Item}; + false -> {BestKey, Best} + end + end, + {Fn(Head), Head}, + Tail + ), + Result. + max_by(Fn, List) -> element(2, lists:max(lists:map(fun(E) -> {Fn(E), E} end, List))). +max_by(Fn, Comparer, [Head | Tail]) -> + {_, Result} = lists:foldl( + fun(Item, {BestKey, Best}) -> + ItemKey = Fn(Item), + case Comparer(ItemKey, BestKey) > 0 of + true -> {ItemKey, Item}; + false -> {BestKey, Best} + end + end, + {Fn(Head), Head}, + Tail + ), + Result. + +min_with(Comparer, [Head | Tail]) -> + lists:foldl( + fun(Item, Best) -> + case Comparer(Item, Best) < 0 of + true -> Item; + false -> Best + end + end, + Head, + Tail + ). + +max_with(Comparer, [Head | Tail]) -> + lists:foldl( + fun(Item, Best) -> + case Comparer(Item, Best) > 0 of + true -> Item; + false -> Best + end + end, + Head, + Tail + ). + indexed(List) -> lists:zip(lists:seq(0, length(List) - 1), List). diff --git a/tests/Beam/UnionTypeTests.fs b/tests/Beam/UnionTypeTests.fs index a104330bd9..0b7f60e3b2 100644 --- a/tests/Beam/UnionTypeTests.fs +++ b/tests/Beam/UnionTypeTests.fs @@ -48,6 +48,17 @@ type WrappedUnion = type T1 = T1 +type CollectionOrder = + | Zebra + | Alpha + | Middle + +type CollectionOrderItem = + { Key: CollectionOrder + Value: string } + +type CollectionArray = CollectionArray of int array + type DeepRecord = { Value: string } type DeepWrappedUnion = @@ -74,6 +85,107 @@ let (|FSharp|_|) (document : string) = let (|A|) n = n +[] +let ``test collection ordering uses union declaration order`` () = + let values = [ Middle; Zebra; Alpha ] + let expected = [ Zebra; Alpha; Middle ] + values |> List.sortBy id |> equal expected + values |> List.sortByDescending id |> equal (List.rev expected) + values |> List.min |> equal Zebra + values |> List.max |> equal Middle + + let items = + [ { Key = Middle; Value = "middle" } + { Key = Zebra; Value = "zebra" } + { Key = Alpha; Value = "alpha" } ] + + items + |> List.sortBy (fun item -> item.Key) + |> List.map (fun item -> item.Value) + |> equal [ "zebra"; "alpha"; "middle" ] + items |> List.minBy (fun item -> item.Key) |> equal items.[1] + items |> List.maxBy (fun item -> item.Key) |> equal items.[0] + + let arrayValues = values |> List.toArray + arrayValues |> Array.sortBy id |> equal (List.toArray expected) + arrayValues |> Array.sortByDescending id |> equal (expected |> List.rev |> List.toArray) + arrayValues |> Array.min |> equal Zebra + arrayValues |> Array.max |> equal Middle + + let arrayItems = items |> List.toArray + arrayItems |> Array.minBy (fun item -> item.Key) |> equal arrayItems.[1] + arrayItems |> Array.maxBy (fun item -> item.Key) |> equal arrayItems.[0] + + let inPlaceValues = values |> List.toArray + Array.sortInPlace inPlaceValues + inPlaceValues |> equal (List.toArray expected) + + let inPlaceItems = items |> List.toArray + Array.sortInPlaceBy (fun item -> item.Key) inPlaceItems + inPlaceItems + |> Array.map (fun item -> item.Value) + |> equal [| "zebra"; "alpha"; "middle" |] + +[] +let ``test collection extrema preserve first ties and project each item once`` () = + let items = + [ { Key = Zebra; Value = "first" } + { Key = Zebra; Value = "second" } + { Key = Alpha; Value = "third" } ] + + let mutable listMinProjections = 0 + + let listMin = + items + |> List.minBy (fun item -> + listMinProjections <- listMinProjections + 1 + item.Key) + + listMin.Value |> equal "first" + listMinProjections |> equal items.Length + + let mutable listMaxProjections = 0 + + items + |> List.maxBy (fun item -> + listMaxProjections <- listMaxProjections + 1 + item.Key) + |> ignore + + listMaxProjections |> equal items.Length + + let arrayItems = List.toArray items + let mutable arrayMinProjections = 0 + + let arrayMin = + arrayItems + |> Array.minBy (fun item -> + arrayMinProjections <- arrayMinProjections + 1 + item.Key) + + arrayMin.Value |> equal "first" + arrayMinProjections |> equal arrayItems.Length + + let mutable arrayMaxProjections = 0 + + arrayItems + |> Array.maxBy (fun item -> + arrayMaxProjections <- arrayMaxProjections + 1 + item.Key) + |> ignore + + arrayMaxProjections |> equal arrayItems.Length + + let first = [| 1 |] + let second = [| 1 |] + let values = [ CollectionArray first; CollectionArray second ] + + let (CollectionArray listMinValue) = List.min values + obj.ReferenceEquals(listMinValue, first) |> equal true + + let (CollectionArray arrayMinValue) = values |> List.toArray |> Array.min + obj.ReferenceEquals(arrayMinValue, first) |> equal true + [] type MyUnion3 = | Case1 From 213c6ce1e6915cedaa01e4e885e3fa286538fd78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:47:17 +0200 Subject: [PATCH 116/322] chore: Bump the all-actions group with 2 updates (#5022) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 632e6e356a..b8ef51ee27 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup-cli@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 04ba0a95f4..0348c9f1ca 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@f3b81cdb3070066a47faa9bdf440f4f1dc78b1f0 # v0.89.17 + uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 9f3e56c6be153bc82182b652c64a5b4364ab80ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:03:40 +0200 Subject: [PATCH 117/322] chore: Bump the all-nuget group with 2 updates (#5021) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/fable-standalone/src/Worker/Worker.fsproj | 2 +- tests/React/Fable.Tests.React.fsproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1df8f9115e..58ff7c3df9 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.2", + "version": "8.0.5", "commands": [ "fantomas" ], diff --git a/src/fable-standalone/src/Worker/Worker.fsproj b/src/fable-standalone/src/Worker/Worker.fsproj index 6db8372780..a86190c262 100644 --- a/src/fable-standalone/src/Worker/Worker.fsproj +++ b/src/fable-standalone/src/Worker/Worker.fsproj @@ -11,7 +11,7 @@ - + diff --git a/tests/React/Fable.Tests.React.fsproj b/tests/React/Fable.Tests.React.fsproj index 6d63e41769..cfee412f9a 100644 --- a/tests/React/Fable.Tests.React.fsproj +++ b/tests/React/Fable.Tests.React.fsproj @@ -28,7 +28,7 @@ - + From 2b08743ae9dc7e0ef1b3880c8843b988af436f53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:04:08 +0200 Subject: [PATCH 118/322] chore: Bump the all-pip group with 2 updates (#5020) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 +-- uv.lock | 80 +++++++++++++++++++++++++------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a8b8669df..27357ae6e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.8,<0.17", + "ruff>=0.16.9,<0.17", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.5", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.82", + "ty>=0.0.84", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 0966a09892..b1b657365f 100644 --- a/uv.lock +++ b/uv.lock @@ -68,8 +68,8 @@ dev = [ { name = "pydantic", specifier = ">=2.13.5" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.8,<0.17" }, - { name = "ty", specifier = ">=0.0.82" }, + { name = "ruff", specifier = ">=0.16.9,<0.17" }, + { name = "ty", specifier = ">=0.0.84" }, ] [[package]] @@ -259,52 +259,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.8" +version = "0.16.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, - { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, - { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, - { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, - { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, - { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, + { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, + { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, + { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, + { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, ] [[package]] name = "ty" -version = "0.0.82" +version = "0.0.84" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/df/b9f35d0b8860f1bbfcb852fbef363301b5ca4b5590cadc6f2a810646d579/ty-0.0.82.tar.gz", hash = "sha256:586e3bf784cece42113929bb64b4761bed5c2127ae6cea294bb82da670066356", size = 7325958, upload-time = "2026-09-17T19:42:12.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/0e/259760638b7b1a875743f1f3975253392932ff74397c8e174df7bfb143c3/ty-0.0.82-py3-none-linux_armv6l.whl", hash = "sha256:be58e089e317e1ada7a655216828e76d369386dc958f7a3b3d455ab6dc7f02b6", size = 13833734, upload-time = "2026-09-17T19:41:35.134Z" }, - { url = "https://files.pythonhosted.org/packages/70/f6/c8a1f70989cf44cf2cada5527e268b4216015a7f28f1d8817e4ef1e84a52/ty-0.0.82-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c080ba9a816cfbb3e34c0d3a0071829c602d14bb992bc799d885d4b30008b4ab", size = 13449689, upload-time = "2026-09-17T19:41:37.65Z" }, - { url = "https://files.pythonhosted.org/packages/d5/eb/31fc4bdecb2ff0053443d4ec1e6719a3f3171636ad6da7a8a642c879a4f4/ty-0.0.82-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f08bd5b751c3b2c46eabe8aee890e7fa54e3bcd97ffa4b8338b0a2508d928c2", size = 13410140, upload-time = "2026-09-17T19:41:40.024Z" }, - { url = "https://files.pythonhosted.org/packages/e7/14/e2af8c64004cd891691915b1522f105a5d601dd8d76a635fec112911f251/ty-0.0.82-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f5ef0401743c2eeac8039ef9ff1bc5dd612bd17ed0259b9b0794e19696548a6", size = 13435552, upload-time = "2026-09-17T19:41:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/52/92/b9519a7854189aa8da4309ecb9c6ff5ab8ac91a433e201322b15c98f9388/ty-0.0.82-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c899643f2a4c1782c06a4bf4959909d1aa2d9d2cac7c1ac3de3161dfcb6909f8", size = 13601427, upload-time = "2026-09-17T19:41:44.242Z" }, - { url = "https://files.pythonhosted.org/packages/ce/be/0c7a39e277a25bca21bbd2dcf640eb4d80cf98f318ef611092cda1222399/ty-0.0.82-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aa6e4e157c6e0b770c327a5d966371b2a7bc7ad4f22d662ac068623cba00eb", size = 14457370, upload-time = "2026-09-17T19:41:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/6f/33/86ff8f1e15c71096838eb0fdfe0f5b578dc717575de6cba591d96baf3b06/ty-0.0.82-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28c141ab481748f1b889eb572e803172ea452af621e514c30a2f30884753b020", size = 14888674, upload-time = "2026-09-17T19:41:48.6Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d2/a1c6315907afd080b4e98d2972441b4803a70c9663a8c7a7f6fd554c7361/ty-0.0.82-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16fff2729fbaa3998ba1cab536dc7d176aef28927b09200588e50982394c56df", size = 14670369, upload-time = "2026-09-17T19:41:50.963Z" }, - { url = "https://files.pythonhosted.org/packages/ae/cd/956ceae4dbe0b116ebc9ecb75dcbe9a0294c50146f76c4c59004e6eb2d77/ty-0.0.82-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2024a33209b37525ffa85a29036985e9ef53a013c0ff321231bd8b23987cf71", size = 14107030, upload-time = "2026-09-17T19:41:53.343Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f8/6025627cec4d6370fb34a2c5cdcf36d89dd4e4ac75b023bda68231e17dfc/ty-0.0.82-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14a0ae8591e08aca5911b545d100e51c9b796350a85b2b36d82219820552d662", size = 14485892, upload-time = "2026-09-17T19:41:55.745Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b6/016f9b36096a9ccd0492ee000380bd75fb51dc4ec7bfef4d6d93f6b3ec2e/ty-0.0.82-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2964202b09108c72232760119c2680cefc33191c428b09e6f620b6c0ab9c6b0f", size = 13383907, upload-time = "2026-09-17T19:41:57.937Z" }, - { url = "https://files.pythonhosted.org/packages/b4/19/1ccc2429399d6ef3d98180340c7abc6793bd90c5039ad41a9003cb8de6df/ty-0.0.82-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f4ed8fc714e35dc11ef4209832e73ad461cf29c9161099eb6c2c541c6b9a3e0e", size = 13617005, upload-time = "2026-09-17T19:42:00.018Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9d/a4a487db72e78413bba2ee9cd49f79b8f461be93e7f6c4dd790febae1cd7/ty-0.0.82-py3-none-musllinux_1_2_i686.whl", hash = "sha256:32a3fe7b1a50f20679fe76c85c0087280419365d900300898a7b95cbcfe510fc", size = 13906545, upload-time = "2026-09-17T19:42:01.942Z" }, - { url = "https://files.pythonhosted.org/packages/0c/4e/a58d530c0f5abe7b846e2d05af9e3f1fb44a1b620ae0abe3892cd3f9596c/ty-0.0.82-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f751044ae63ea9e73ce2eae4c23a839efa4a04d6487d6ea411a590fbe43360b6", size = 14248518, upload-time = "2026-09-17T19:42:04.164Z" }, - { url = "https://files.pythonhosted.org/packages/a6/db/5505229835171d19adcaaeec3a15e0b533b578d24de1329db5da9306c4c6/ty-0.0.82-py3-none-win32.whl", hash = "sha256:0eb66c5ce58434a48d2cae2780d25285c7ca19e07a0d38b1fe39d1a0f75d6fa3", size = 13059890, upload-time = "2026-09-17T19:42:06.161Z" }, - { url = "https://files.pythonhosted.org/packages/13/22/faa427b65baf39915f52a6fa079ece4253e831f3364c567bf91bc1eef65a/ty-0.0.82-py3-none-win_amd64.whl", hash = "sha256:1e4a6c1ca2dc1a6929517d73a47cd626989bb8bd67811425fbe022d06eb2bf73", size = 14003730, upload-time = "2026-09-17T19:42:08.268Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fc/3b95100993d28acb2f3bcf3a49846985f5b2aafab6562627d018f6d23788/ty-0.0.82-py3-none-win_arm64.whl", hash = "sha256:a6f0e8a1f961518746edbccc5986945c01a56a917ebe37117aa028a038d54ae3", size = 13567713, upload-time = "2026-09-17T19:42:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, + { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, + { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, + { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, + { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, ] [[package]] From 946f5c75b60ec9cc99328468c9d04f19b13f8175 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:04:38 +0200 Subject: [PATCH 119/322] chore: Bump hypothesis from 6.168.0 to 6.168.2 in /src/fable-library-py in the all-pip group (#5019) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 168 ++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 82 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index db8b5315f3..24c90c9a1c 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,94 +61,98 @@ dev = [ [[package]] name = "hypothesis" -version = "6.168.0" +version = "6.168.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/ce/c0946bebffb99b62426a6a7643d4272cc6c5cf777a488b3b4d0ee724e960/hypothesis-6.168.0.tar.gz", hash = "sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e", size = 510805, upload-time = "2026-09-08T18:48:36.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/10/5795cc413d2ba5ff9c81e5d1b27ba3c652ac6dfac6b1d67072052d338391/hypothesis-6.168.2.tar.gz", hash = "sha256:df61fcada928a3fb3fba4602be5b2d569eb50c36fddae0d2e2a5f51419d0ce9b", size = 510987, upload-time = "2026-09-27T05:22:19.959Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/f8/8b2cc9ae7b439538f6f2d32a92892340b6343a6b04d171c516666901dedc/hypothesis-6.168.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2", size = 791358, upload-time = "2026-09-08T18:47:37.076Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f4/4d7d897310cde5085779fb96feadb8529d98cb8e51ed7b24f7da9b6c6bdc/hypothesis-6.168.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099", size = 787081, upload-time = "2026-09-08T18:47:16.227Z" }, - { url = "https://files.pythonhosted.org/packages/26/7b/9d52066d363faba7f3ac20ee60a3c696a475feb2c477d235d0d649d41cc1/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4", size = 1123850, upload-time = "2026-09-08T18:48:11.504Z" }, - { url = "https://files.pythonhosted.org/packages/50/cf/aa46d76fa7df43caf2c372e394fda84ce1dc08421814f8674a6b9295e2ea/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f", size = 1147685, upload-time = "2026-09-08T18:46:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/84/c2/78ed8c8d5aa37e4baae2a4b3e29687ee3d9b7f1a5e56ea5ea5ec7ec71ecb/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1", size = 1149294, upload-time = "2026-09-08T18:47:04.757Z" }, - { url = "https://files.pythonhosted.org/packages/78/7f/d57440f19e9de70e85359cf179ce786f309ee17609bd3c5a0113272875c0/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213", size = 1169729, upload-time = "2026-09-08T18:47:44.056Z" }, - { url = "https://files.pythonhosted.org/packages/a1/86/dc74410a186990bb22c2a3eea0e77804f2eb0f300860b46d7d8948073674/hypothesis-6.168.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131", size = 1129182, upload-time = "2026-09-08T18:46:33.382Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5e/be048fc4f6dac831625e155bdf11e4233caf46bb54030391d6fba8e19449/hypothesis-6.168.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149", size = 1160180, upload-time = "2026-09-08T18:47:26.459Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4a/15a34498a5f08720fbbdbbe4668a8050fe4e17c16c9eeb6f56a017f2fa6b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d", size = 1299711, upload-time = "2026-09-08T18:47:28.34Z" }, - { url = "https://files.pythonhosted.org/packages/77/09/5354e0dae302ab98c4f0046b7e8699c2186ae4349397bda5d5c852bda68b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517", size = 1425341, upload-time = "2026-09-08T18:46:56.785Z" }, - { url = "https://files.pythonhosted.org/packages/59/7c/3c0e1f59043ff128c70a51d298d3d6b5973525c357a1e1d0542dbc05ac90/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349", size = 1281063, upload-time = "2026-09-08T18:46:55.401Z" }, - { url = "https://files.pythonhosted.org/packages/40/dd/db884db9a7d42ae6b72a00c13c725940b638dfb263e618b22af59d5dfa2f/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b", size = 1300247, upload-time = "2026-09-08T18:47:02.949Z" }, - { url = "https://files.pythonhosted.org/packages/99/8a/4ee9769e1d48676efb6a78a130f82e0d52d3f87b2055294102272a08615c/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377", size = 1336084, upload-time = "2026-09-08T18:48:05.675Z" }, - { url = "https://files.pythonhosted.org/packages/61/17/d4ed11bc99d205d6d2651a0f1a4874f377150f836b5a0849bd511d68a2eb/hypothesis-6.168.0-cp310-abi3-win32.whl", hash = "sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29", size = 677989, upload-time = "2026-09-08T18:47:24.722Z" }, - { url = "https://files.pythonhosted.org/packages/77/51/abf1fde7b8afab87db30afb73b3847e62440551d146472111cabeba2fe00/hypothesis-6.168.0-cp310-abi3-win_amd64.whl", hash = "sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b", size = 684692, upload-time = "2026-09-08T18:46:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/12/e2/64d79aed47a95186ce9edcef60eb4684870c72542da3b7027c2483d8ee8c/hypothesis-6.168.0-cp310-abi3-win_arm64.whl", hash = "sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc", size = 682709, upload-time = "2026-09-08T18:48:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/95/67/bfacadcc6cd4bd179d1cd2f6c85f580136019b38af7db71e3159ebb3ce33/hypothesis-6.168.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5", size = 792193, upload-time = "2026-09-08T18:47:42.392Z" }, - { url = "https://files.pythonhosted.org/packages/fc/29/4abab4114035f54438e3b07bdab3f68fd8769fe0fb84f8ffd6656eaa58fd/hypothesis-6.168.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc", size = 788121, upload-time = "2026-09-08T18:47:09.398Z" }, - { url = "https://files.pythonhosted.org/packages/85/d3/c54242439dcb59c7e3274a2952188bebbc605be397c3a6ff027fc81e1726/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3", size = 1124222, upload-time = "2026-09-08T18:46:28.345Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8b/1c9f54be7040fe7d94e4cd5d9343da5a36240bee2fa856f44109f9622251/hypothesis-6.168.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67", size = 1170346, upload-time = "2026-09-08T18:46:52.784Z" }, - { url = "https://files.pythonhosted.org/packages/af/d4/f6b1557bd3cf2976d56465fcbc483214bc6e7f1aa7e4db9f8c5f776d547a/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0", size = 1300267, upload-time = "2026-09-08T18:46:45.644Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ad/a345588acc038deb09a43610328004d31fe5a53289627e03b0199978dc45/hypothesis-6.168.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f", size = 1336724, upload-time = "2026-09-08T18:46:44.179Z" }, - { url = "https://files.pythonhosted.org/packages/97/e9/c5e380fd29fd552ff442bcfb1090926287e2911d40212ae0602a0736347b/hypothesis-6.168.0-cp310-cp310-win_amd64.whl", hash = "sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236", size = 684644, upload-time = "2026-09-08T18:47:45.814Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7a/7a28d9afd52c9a89c4d8e3170c92fe1ffbd4a4ce3f905ca15fa7ddaaa581/hypothesis-6.168.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a", size = 792045, upload-time = "2026-09-08T18:46:29.612Z" }, - { url = "https://files.pythonhosted.org/packages/0a/40/e2d6fe12b54abbc8b802b234ec4974d0a420dde4098f57a324e9c791ac27/hypothesis-6.168.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9", size = 787932, upload-time = "2026-09-08T18:47:11.145Z" }, - { url = "https://files.pythonhosted.org/packages/ff/89/cc78e05f3248949c6d9ecb489d0e422247d11b08533ab02e177a10a01610/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843", size = 1123994, upload-time = "2026-09-08T18:47:12.647Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e3/7d483a79e2ed9a6c868cac8783b3c6c2bb82303f7db364c9da9a406df10d/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063", size = 1170271, upload-time = "2026-09-08T18:47:54.322Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/bffaf5b5e6b4566aa40d8a4ae25d8d84b47dfe0d9e771f454e4974cdb428/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04", size = 1300162, upload-time = "2026-09-08T18:46:27.022Z" }, - { url = "https://files.pythonhosted.org/packages/25/06/6c30a00fc2f5c58546e858259cf2261c6a3ef6a060c373e00d00311267be/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f", size = 1336294, upload-time = "2026-09-08T18:46:37.324Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/501910e5a6a9c245f2394fda016376f6c7e209c5dca8c056eaece64ad10e/hypothesis-6.168.0-cp311-cp311-win_amd64.whl", hash = "sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2", size = 684464, upload-time = "2026-09-08T18:47:19.779Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5e/0035896c101f0484c364353f8ee30175eef8936b49171618677287fdd85d/hypothesis-6.168.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda", size = 793115, upload-time = "2026-09-08T18:48:21.352Z" }, - { url = "https://files.pythonhosted.org/packages/11/5c/938173e27df771cc6e92bc47f117a1b1be4a88fc6dc214f7fc65f9c7ad93/hypothesis-6.168.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79", size = 784634, upload-time = "2026-09-08T18:47:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/0b2c6ac07d519131f97712f2533750ffe9b2490eec8f518ef3a5ed2dd514/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7", size = 1122855, upload-time = "2026-09-08T18:46:21.967Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/dde930fe43171cab37572bd993d70c2a271f240f428f8ccd64ec4c2d661b/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4", size = 1168932, upload-time = "2026-09-08T18:46:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/4c/5d/92b83c3d06194ec626e92723d0b0f70221ebf42d7cb355ed36929df6d735/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d", size = 1298566, upload-time = "2026-09-08T18:48:19.485Z" }, - { url = "https://files.pythonhosted.org/packages/9c/67/52de8bf3446e3d2d555b812d96673b31bb213b5b9d5804a666e5d0bba76e/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d", size = 1335074, upload-time = "2026-09-08T18:48:01.561Z" }, - { url = "https://files.pythonhosted.org/packages/14/fd/e592773c1c0ce55e35d26ec55f75546bf1fd72ef5a5c520ed685969b40cb/hypothesis-6.168.0-cp312-cp312-win_amd64.whl", hash = "sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68", size = 682026, upload-time = "2026-09-08T18:47:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e9/39bb8fcccfbafd10fcc777d583c6ebad5d2148e7d743cb350562f28e974f/hypothesis-6.168.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28", size = 793050, upload-time = "2026-09-08T18:47:56.045Z" }, - { url = "https://files.pythonhosted.org/packages/f7/dd/00fd32e8ec470535e0065cb8d6e175f9fc54b4d3a6f1269f6c487f8bd79d/hypothesis-6.168.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20", size = 784558, upload-time = "2026-09-08T18:46:32.054Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4d/553c47093f68bdbac0438e16c024ce97649b5804dc6972ef86b9bd2db8a1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475", size = 1122841, upload-time = "2026-09-08T18:48:09.374Z" }, - { url = "https://files.pythonhosted.org/packages/43/d6/0b5940aa75e617c8fd12200bae24d1b71347362514e8210735c581d4d3d1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0", size = 1168825, upload-time = "2026-09-08T18:47:33.702Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/800de1231b2869b51409bbf85799d6f1bf49a00e0afff0aabc097aa8f1b7/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669", size = 1298433, upload-time = "2026-09-08T18:46:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/be/35/9907667a30c1dbabbc44a09b4c25a0f575570937fcbbada924ae3a1dbf2a/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c", size = 1334988, upload-time = "2026-09-08T18:48:29.395Z" }, - { url = "https://files.pythonhosted.org/packages/98/8a/7bf214e703fff532ed47cb52ab93ce4b7ea41e4e7084593fa02b740828b7/hypothesis-6.168.0-cp313-cp313-win_amd64.whl", hash = "sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d", size = 681990, upload-time = "2026-09-08T18:47:49.173Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c1/64b36b250b1f66abb6ce8c81775d3373d149bc89cbea477ed71b57cf7d1b/hypothesis-6.168.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc", size = 793101, upload-time = "2026-09-08T18:46:54.067Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c4/494e42304b15f4ec649d36bbc3fc01cef1b63405cf30d4087ae07d048172/hypothesis-6.168.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8", size = 784632, upload-time = "2026-09-08T18:48:31.551Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/90d874cb1d749f505749c9908f34e803b97b03457797d5893354980558bc/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93", size = 1123101, upload-time = "2026-09-08T18:48:17.362Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bb/ec893d0e5f4bcdd0121a8a4280f4e8aba3b4cdae01411f3236016ecd1f81/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7", size = 1168962, upload-time = "2026-09-08T18:46:48.507Z" }, - { url = "https://files.pythonhosted.org/packages/11/f1/16ec2bddbaed461725d9aa5a80b43f1905ea50a08f689ec46f8966bb4f0f/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9", size = 1298932, upload-time = "2026-09-08T18:47:40.632Z" }, - { url = "https://files.pythonhosted.org/packages/8f/12/7c2fe2706d092f12bd7b3e8565e1ca5d0c24b853751f2f970768086dbdeb/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6", size = 1335196, upload-time = "2026-09-08T18:47:57.775Z" }, - { url = "https://files.pythonhosted.org/packages/20/35/59f7ca2414ca39408d13f66a344affe0ffc64748dc01d8a1ca910009cdcb/hypothesis-6.168.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27", size = 624102, upload-time = "2026-09-08T18:47:38.869Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1e/dcd9335ace916ffea40f2cb04ba4122094c2f7b928f3a73fc7d452ce5b71/hypothesis-6.168.0-cp314-cp314-win_amd64.whl", hash = "sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193", size = 681871, upload-time = "2026-09-08T18:47:59.697Z" }, - { url = "https://files.pythonhosted.org/packages/de/d0/bc50b0b91e40744b7caa56b8add85cef432f85b4d00108409e8eb17af830/hypothesis-6.168.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1", size = 791695, upload-time = "2026-09-08T18:47:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/4b/53/fc7537d50ff008dc5ea8598764935f93dd07bcedaf23ee4e635bdf7055f4/hypothesis-6.168.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd", size = 783239, upload-time = "2026-09-08T18:47:47.43Z" }, - { url = "https://files.pythonhosted.org/packages/71/2a/c7aac2efc06713f704d7e354755aff4a11608b9fe93d973ead374f3b81a3/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507", size = 1121412, upload-time = "2026-09-08T18:47:07.927Z" }, - { url = "https://files.pythonhosted.org/packages/60/e2/668ab29e5096af682b17b8491f5427d7c5f17c1b991daa5577bde80c29ca/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4", size = 1167570, upload-time = "2026-09-08T18:47:17.94Z" }, - { url = "https://files.pythonhosted.org/packages/3a/17/c64635e4c988b5fa3d3b8be322e19e2fe4c731fa0ca074ca852aa70debea/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8", size = 1297118, upload-time = "2026-09-08T18:48:13.407Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c5/c7a0d9a06bf5c3279386dd53161081a57b98c6faf60fbbf64d046315e9e6/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571", size = 1334068, upload-time = "2026-09-08T18:48:15.462Z" }, - { url = "https://files.pythonhosted.org/packages/32/99/11a393a20a867e9b978308323d45022e96f5cd2edf391e4d9a65fb4e2cf7/hypothesis-6.168.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8", size = 681795, upload-time = "2026-09-08T18:47:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/16/f7/5adae1bf1d4877aca2e8c8e007e57077237e9ac3765e430ffda490b17e19/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128", size = 791056, upload-time = "2026-09-08T18:48:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/f2/93/b1b2770b87591db5cf564b9aa0265cf21e9207d28645e0abdeb8df63225b/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795", size = 782980, upload-time = "2026-09-08T18:48:34.087Z" }, - { url = "https://files.pythonhosted.org/packages/de/bd/673171c1d2423379a7d4a0f9f009cca735a422d0c4a4ac6422d1d1736cab/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441", size = 1120964, upload-time = "2026-09-08T18:46:59.92Z" }, - { url = "https://files.pythonhosted.org/packages/7d/00/33a9bd941b22a4fd8a8c805b1563e0db17efc020422f5bd15bdd0fdf258f/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63", size = 1143869, upload-time = "2026-09-08T18:46:49.875Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c1/963460976f41721eff8f67f30d31059ea407cc1b41c737c8859aabf37197/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782", size = 1146453, upload-time = "2026-09-08T18:46:25.757Z" }, - { url = "https://files.pythonhosted.org/packages/ce/53/09db238098ad66f4e6d2fe883f26c270c2595b90e21c8f969d4cb21cad7a/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc", size = 1166918, upload-time = "2026-09-08T18:48:23.436Z" }, - { url = "https://files.pythonhosted.org/packages/b9/31/e1b7b452c8a6166e445ba2ad80a864f6a9eee0fe4c8cecdb9af5c1ee0aa5/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e", size = 1126637, upload-time = "2026-09-08T18:47:50.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/2b/4eceed248afb46fb6b2df21cf2239362de5b25d295c2dc67a82ec8657d5e/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c", size = 1155682, upload-time = "2026-09-08T18:47:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3e/f3414cda4f325004d5774485e8983b7d1b99e4b91013f013dd088fd778cf/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9", size = 1296464, upload-time = "2026-09-08T18:46:34.833Z" }, - { url = "https://files.pythonhosted.org/packages/aa/40/ca79cf96545e1f172b36b8df56bfeb02b61f351f283026f9a57c4631e368/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad", size = 1421853, upload-time = "2026-09-08T18:47:23.083Z" }, - { url = "https://files.pythonhosted.org/packages/77/bc/657d5386740c1f4ac518ff05e4c5b132f1e6df2e7c865ba8fda4279adfa5/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f", size = 1278216, upload-time = "2026-09-08T18:46:30.871Z" }, - { url = "https://files.pythonhosted.org/packages/51/54/2328cdb70489a36634534478d9b238594a269d8bec4630ea0e6897048347/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09", size = 1297593, upload-time = "2026-09-08T18:46:41.395Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ca/d803fa57e3ff7f460f6e262d2b74822cf143343d378501fd3da601b12040/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887", size = 1333785, upload-time = "2026-09-08T18:48:25.434Z" }, - { url = "https://files.pythonhosted.org/packages/86/8f/b9799ae6ba6074f151db2c63f9f6f12d844512821ffaba1a3672d7e59f07/hypothesis-6.168.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780", size = 675173, upload-time = "2026-09-08T18:47:52.601Z" }, - { url = "https://files.pythonhosted.org/packages/61/54/14c3e277b451ff24128ecc2673cac59dd7e535bce1a433c466912fd682e1/hypothesis-6.168.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef", size = 681491, upload-time = "2026-09-08T18:46:51.437Z" }, - { url = "https://files.pythonhosted.org/packages/99/f3/827e4a48ffee7e40244b0bf064ba47c2171e053ab7edf1cf770105e23401/hypothesis-6.168.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a", size = 679197, upload-time = "2026-09-08T18:47:32.112Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/7f15e1b10d13e266b0f09cf3117dd2206104a1f6ac3b3baf6ec4c43b7c2e/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76", size = 792946, upload-time = "2026-09-08T18:47:35.438Z" }, - { url = "https://files.pythonhosted.org/packages/10/fb/32487bdcf68b3805ec5efcb7f92fb002b573ad3f90c252d2f2913c5d3124/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01", size = 788783, upload-time = "2026-09-08T18:46:42.862Z" }, - { url = "https://files.pythonhosted.org/packages/13/26/1aa42e0069ebc68d29d980d47420af324fa002494e01c1c0321c34f609ba/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab", size = 1124762, upload-time = "2026-09-08T18:48:07.512Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4e/99509045ed55aaa05d8408663051a93168d1a8a5bfc132d92ec1584ce47a/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7", size = 1171748, upload-time = "2026-09-08T18:46:47.029Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c9/96e57dfd89913322b5180b031b9e36a29bfe67842108b19904466476cd24/hypothesis-6.168.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c", size = 685447, upload-time = "2026-09-08T18:46:35.97Z" }, + { url = "https://files.pythonhosted.org/packages/21/8a/1a244014d639f3d21e419a8207b7f1c2d8f4caf6e14073c75bfb364b27e3/hypothesis-6.168.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:14d857ee2669e76d7aee7fa56a2f13d40fc6ba8a34c7b340fd53744a7623f9d3", size = 791551, upload-time = "2026-09-27T05:19:59.476Z" }, + { url = "https://files.pythonhosted.org/packages/17/47/fa17a1a3cc971bf454d2b4fd0ccc2d35ab2d4b705efbc5e4149b70aa4072/hypothesis-6.168.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1ca4205f8bff16383a35924ec6b87ec649a407d204c9377da225924d695aab23", size = 787291, upload-time = "2026-09-27T05:21:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/a5/79/989e20cebec9fef907c43e34dae9c0938b49e2d3bb8537a79990692471c0/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8268bcc733f2b93e77f64e54d6fa61db79af91806e2d26b950f5d1cca47b4bfa", size = 1124033, upload-time = "2026-09-27T05:20:17.522Z" }, + { url = "https://files.pythonhosted.org/packages/ec/44/3b5edc918128eccea902eb6ee91f0c58ad575274b99c565107fc575483a9/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c138d9848c210b8a1e511ab0884f16bc71470400b11255e8b595df320809afc", size = 1147868, upload-time = "2026-09-27T05:20:44.835Z" }, + { url = "https://files.pythonhosted.org/packages/2a/54/04d30544681626fb7fa25cc1041c12ebfad79aa4995438f6f3e577541111/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:420479d419d73dd7bb2ed0cfb57f35b1dc0bc246c30b250904c3987c5cae9463", size = 1149472, upload-time = "2026-09-27T05:21:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/a7abe9660b4b997305537aa860fb932f97223125713655a7a1e9a302f629/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01f74944abe6988a6213dd036bd2dbf1e4f0513af0cf66b7780773fdf0c6f5e2", size = 1191885, upload-time = "2026-09-27T05:22:15.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/24c77c25ec62941022edcbb98a052b98c57b60fee7d289cabea73523a727/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe84b8218a4fc33f79e64840d23fe9c627e7acb7cad1f70bf6c0f163c7f4f01f", size = 1169899, upload-time = "2026-09-27T05:20:13.379Z" }, + { url = "https://files.pythonhosted.org/packages/cb/23/dd129e570f44051de911761cada83db5695bbbe3db450bc905d449ea8aa2/hypothesis-6.168.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:ce74728a05d5e5dd4d28bb4fbaa0c1f5e809ebf6be54d343128d970ee1025697", size = 1129354, upload-time = "2026-09-27T05:20:10.768Z" }, + { url = "https://files.pythonhosted.org/packages/71/65/6d911e5b270c7f9560929b07304869a1f77e2440e22db107600788975bbb/hypothesis-6.168.2-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29e12bcfcef363a9dbd0d3d62fd9608853268d15319ae3ecaf802555497392a9", size = 1160386, upload-time = "2026-09-27T05:22:05.475Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/5cb26f7a2323ecdb4881f06c8eec19c4087e04b23cc7998b74f2b565a66c/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3cbe12b2427a86d769fdc93242ee3207d3a1523798b856630f16b674ebae17b1", size = 1299894, upload-time = "2026-09-27T05:20:02.55Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f3/eb40e5c554b78b1b500b93172829b4b16c677acadd3f10f979117a82f851/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3b62ce43973f4425530e590751439bacceec19921af0edf9fae3a63e173ea146", size = 1425524, upload-time = "2026-09-27T05:20:58.58Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/03819636fa86ade2db91ad9953851b49f885a3481a22f0b1361d0c5dfc8d/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:51df0068106dd2db4fc5a1e56801adf5fc1d354792584289a9cc8ac09273d376", size = 1377070, upload-time = "2026-09-27T05:21:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/5b/80/505146f225bbd24759651a16c17cd44aa6f082fa4a14761646e8ec37f374/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:e54638a0f66c608840af13f3ad665710e706bda5c52f706351bb83b5744bec63", size = 1281230, upload-time = "2026-09-27T05:20:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7b/3c9f0f1cf0733b6d692dd5dca92037f6b707082bfb4c29712f2d0258049e/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:84c5b9891f979e851d140618783e9c3624352760dd78cba4759b263686cdad16", size = 1300413, upload-time = "2026-09-27T05:20:14.695Z" }, + { url = "https://files.pythonhosted.org/packages/81/dc/56c1626009545b86fefa71ee71d273fe518d1374beca40bc9b784818a58e/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a6ac0836daba2da486a4fefc22fcdd794dac350bd787a1bb896748cdc47e05e", size = 1336267, upload-time = "2026-09-27T05:21:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ea/44/4b4b58350d93f910165b6b799e757735d0880f624a189b32e8cd4214d8cf/hypothesis-6.168.2-cp310-abi3-win32.whl", hash = "sha256:ac8e27fa3b5a97d692db528a808678181d931056c76cc7901c22329a58ed5e71", size = 678191, upload-time = "2026-09-27T05:21:18.427Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c2/f7e7f3c895df3182f78bca8e3564fa169d8f5b2ee2880a6394bc8c86f22c/hypothesis-6.168.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e9a03e9c1a725118986ab8b65a7b9ef090fa6e88e882897e4f8351865a53834", size = 684899, upload-time = "2026-09-27T05:21:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f1/2d215f89caf7b881f8e7eac4796e3f7080d0324d8f62d782e0becf5df1d8/hypothesis-6.168.2-cp310-abi3-win_arm64.whl", hash = "sha256:60212566e2c4877f3aa9f5c754b07cb1f087c943fdf91cd13e28fd490b3099b3", size = 682893, upload-time = "2026-09-27T05:21:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/1aafd1d38294bf0ba630218aad14b810e96ab3cad46753119abf75740cbd/hypothesis-6.168.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:656ecb3028fc88dd5714592de3bd6dce68224b1387e7664d96854ea10f7d8140", size = 792310, upload-time = "2026-09-27T05:21:08.548Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/c6ccc50a5d76d22a89b3fa639d3a903007c1d210824204e651ee455e4770/hypothesis-6.168.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3be0148392a9326436ed656ecb4ee646a78213a5e391aecbbdc9226cf4532a28", size = 788321, upload-time = "2026-09-27T05:20:37.249Z" }, + { url = "https://files.pythonhosted.org/packages/de/9b/5a7f40ce83d41d4bc82ad699c132b4289e949784bb7e4a4172fb32395eb2/hypothesis-6.168.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371a9d749dac3316697b099dad274a6f9d9ea860b73ccdef4936859fa9dbb8e", size = 1124421, upload-time = "2026-09-27T05:22:11.307Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fb/285c816766713c0af37dbcbe99e9e3c81259a2a3ac3c84fa6fc2348add6c/hypothesis-6.168.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c5c743ab68f567f7be51d402301121a10dddffbdcbd3072282035b80f8a7bb", size = 1170623, upload-time = "2026-09-27T05:22:07.462Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/9cb54cd41874ce8dd7e7879ccac5aa5ea1a46e5857ad50df8e2a85cf8bc5/hypothesis-6.168.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68920b82b53f9318b7c881b2386bad1b2437f1f79d4681363c6c41a81b9d62f3", size = 1300440, upload-time = "2026-09-27T05:22:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/d3/14/9af1bee77064d075e8adc25a48bd967a5eb0f7807d088a9a3a1937b7aafb/hypothesis-6.168.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1e17436910f43db278363571d92e72a4a45f6e2552919a154a50f3b3f2120c4", size = 1336902, upload-time = "2026-09-27T05:20:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/a98f38cc794717476380d28868911e42a80f3f49a2505235f51e9e4a4e51/hypothesis-6.168.2-cp310-cp310-win_amd64.whl", hash = "sha256:69a5492a45500afc7e0263678cf872a3df4234075a8370c45348b2e6282735ae", size = 684844, upload-time = "2026-09-27T05:20:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6f/d73a4e028552df5699eae91af65da0442ee494846ead5c394682a19e22f9/hypothesis-6.168.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:502a2a9e72f20bc61071713d140f0cb21f50ca8736d9286077af0d00c38c7541", size = 792224, upload-time = "2026-09-27T05:22:09.37Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f4/b6d21f72d4e38762d6ec56fc6e8739b78e13904a4790c4f10ffda39daa17/hypothesis-6.168.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f074d433004135f6c03047da508701481ebdb0c0829be3f0e8e20379c05232a", size = 788103, upload-time = "2026-09-27T05:21:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/60/c0/f24cec4cdeb1fc1c1c8e6aa06f0e8f871e03f12afd2808279ab36fdfe0f0/hypothesis-6.168.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d193d435cf3fa604f1be94ecfa2936bf6ed78db33cfb7da84e9feb57b45e12", size = 1124229, upload-time = "2026-09-27T05:21:36.634Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/5475eb96154d20160bf62c74076b5b56744dcb8984836dce6e068a5028e9/hypothesis-6.168.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21d98055721e8ca1604dbcac18f7329bec4793cbf14b21081c3cdf471db9255e", size = 1170405, upload-time = "2026-09-27T05:20:31.066Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f47e5adf68cbcb1c89a75854a3d5048e9ad51c7daec60b676ed2a27fef76/hypothesis-6.168.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:584979873b2bd375a4194252233b12cb237ee57b28acd6c1aa846fc9d9dea85f", size = 1300335, upload-time = "2026-09-27T05:20:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/85/b717012e1f7eed35fa7b10f75674b33774ddc8736fd1bb965f22d8e35abe/hypothesis-6.168.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ec125fb3b07ec8f76160d002c1b4a023c9467e76ba0dcbe9368d51e352bd460", size = 1336470, upload-time = "2026-09-27T05:20:41.909Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/1e1f9fb5fd45dd16aff5cc38f5428150be751a0ee0ff225f1fcbbb79c1e9/hypothesis-6.168.2-cp311-cp311-win_amd64.whl", hash = "sha256:54a76da7694a8c759bb9118523584942d718a7dbb61a321eaf7f085467ae31fd", size = 684657, upload-time = "2026-09-27T05:20:43.328Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f2/c398a808363b3661bb0acd5e3103b7203262e63acef5ccf2acccbad041cb/hypothesis-6.168.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:47064622758ed066ff39bca51ae2dbe7936be9369fb04da7a661b8beea489501", size = 793299, upload-time = "2026-09-27T05:20:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/3c/57/822d48fa61e9bd3049ce5caab399350fafa5f592e8a89bf53fabf798b14e/hypothesis-6.168.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1fdf95461ef7c745d9711a9ac865dc7557bb53a1cc973d2a8f54978ac6df45b6", size = 784846, upload-time = "2026-09-27T05:20:57.024Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/853cc464739bfd076e0f6bac55c4fcff362effdcfc10873bd1a119e80994/hypothesis-6.168.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e28cb8912d7811dcec91b2389d9dcf3b34523c043cfb7454f9c62c8e5a40b2a", size = 1123030, upload-time = "2026-09-27T05:20:38.642Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6a/506d3c0e45bf8cfb6bce2842b0642df37db07f2d5695f4614fa410e854cc/hypothesis-6.168.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3630d664a343354701618a409d3b9d563cb97cfcf749c7a37c5fb32cb3c7084c", size = 1169073, upload-time = "2026-09-27T05:20:55.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/52/244eff24dbaa1f9520dcb42164fb3769e6c08a2e252455af2e8bd23305e5/hypothesis-6.168.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0df395d27012b07eeed98b2e3f2a17f693236b0518d8a086679dc657801aead", size = 1298741, upload-time = "2026-09-27T05:20:32.609Z" }, + { url = "https://files.pythonhosted.org/packages/2e/99/cdcb4fa1b738ef07827464bdf5ff83897f8303e8eca569207cc52f5be2b2/hypothesis-6.168.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:778f043b8af618baabe79cdb15f6f7855ccf950c6911eea84acabca5b7637cde", size = 1335252, upload-time = "2026-09-27T05:20:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f2/09c4e5a334d89d242e9068106cd3286f45f642a7658397bab367c5a9a267/hypothesis-6.168.2-cp312-cp312-win_amd64.whl", hash = "sha256:66d0187f7d81cc4d016e211a8c042b1ac169685ce6ea265acff04e299e189d4f", size = 682188, upload-time = "2026-09-27T05:20:52.329Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d2/64be17ff67f5178b935efac1154f2251fae223b11529431e7e444407041d/hypothesis-6.168.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5c74b4fdd7945130b825018e71515b0b9042ab726ef939dd70984138dfc4eb2c", size = 793237, upload-time = "2026-09-27T05:21:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/8f/09/4e5096fc2b85843abcd7f30e92880fd3a0d08d166c56b468adcdc5cf9c5b/hypothesis-6.168.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37747c599a586ab53819404f4581624290da7f374f892c7f572f9921e90abe00", size = 784689, upload-time = "2026-09-27T05:21:11.902Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/63f0a6b5b709e2bdd0d1d0041c48c6d4614323e27c677a0d44d9748dd4cb/hypothesis-6.168.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97437cf297feb9645c23fd187734c137989c503303542a4034eb43d07f8f151e", size = 1123008, upload-time = "2026-09-27T05:20:01.27Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/284046912c05973f91d41bd7ed79e3bf1917b1332b70e677c3cf2872cea8/hypothesis-6.168.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bd1b6334eaccde40ab56ffb96788f2af993869b08aebc4f1b3ff28f3edcda9", size = 1168965, upload-time = "2026-09-27T05:20:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b7/afe659dccd780393ca6beceabc3a418f11ff2539a83bc4ce9e6c8a0ab589/hypothesis-6.168.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac134d9fe4a1623b57b2a1681e3f51c2c5b324e690c1264dc01a3c57c445b08a", size = 1298607, upload-time = "2026-09-27T05:20:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/d6300216c75fe878da9bc8bfc5e266baa1d350ea145001272d20b3ff3843/hypothesis-6.168.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ba2a0767955fd6491cb93e6917db1d5b718a89f9a4de33b0827b7fce1df289", size = 1335166, upload-time = "2026-09-27T05:20:53.745Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fe/ab19f96bf248571b6e7aef051673c8d7e3b01cba4c40c5f0e2a4be3df398/hypothesis-6.168.2-cp313-cp313-win_amd64.whl", hash = "sha256:9746ac4e78a25be801de86aa386d6e2f95dd6d6b88ce4a1ad639b3d009425fae", size = 682154, upload-time = "2026-09-27T05:20:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/c1b7b533845575f84fa8c20343bf67340bc2d9f19ec8c457cdbb886aaeee/hypothesis-6.168.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:515e0fcad7b3cfc81c93b5d27024b98f5e53506394373fd7e319ee303f294aa9", size = 793273, upload-time = "2026-09-27T05:21:57.508Z" }, + { url = "https://files.pythonhosted.org/packages/7d/76/060863fcdba95cdc1390719034bb767bfde00f399a3763b2fdcac65137d9/hypothesis-6.168.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b55bd7d4af2cea659df6660266d4a70ad947d668685c386d79cc9884a30c17f3", size = 784836, upload-time = "2026-09-27T05:20:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2a/11e427ed3f09f0e1d548428144e97fb9bfbdb2f41b6871880625acf41411/hypothesis-6.168.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db13338672ba689203f405c6258ef62b0590e3d193714b30809e53e8bfc66e84", size = 1123318, upload-time = "2026-09-27T05:21:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/89/03/80911aca76e411cb1d1e7a5deaea0a49b47321ccd85092ceba971100b921/hypothesis-6.168.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:170a6d2d6d9d209992db1e671d0c2ad17367c8046482ebff2cbc9e14a9698be7", size = 1169146, upload-time = "2026-09-27T05:20:20.159Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/243f18213e39924db4be21b53e3999c183d12954745254e717a639fc3490/hypothesis-6.168.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9310d3f997f3d5e8e31d5c5f9946bcd3ccf6eb62c92a1b728f2dd57fd62fda05", size = 1299106, upload-time = "2026-09-27T05:21:06.885Z" }, + { url = "https://files.pythonhosted.org/packages/6a/47/abc5881cf0cdce97663b877f486ff064d834850ea19171ba556c7dd835ae/hypothesis-6.168.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d66e68a0dc9e216f9bdda2c9f066c09e36849cbcb49fbcb1860087560709bb8d", size = 1335374, upload-time = "2026-09-27T05:20:46.633Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f9/08d4bba84eab0404b42971af4ee21c4f9f2064444dc7c8040a14f722ffea/hypothesis-6.168.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f98f7581f68acc659510f8786c39c5cf140dbc3dacba6cd0ed059b0e296a158b", size = 624273, upload-time = "2026-09-27T05:20:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/2b8941a0407da26cf1f789f235cd5dc0e5958b80b7bc12c94596bd24abc0/hypothesis-6.168.2-cp314-cp314-win_amd64.whl", hash = "sha256:6732958300f8994f4b81df11fecf66671f8dd5899de99c3a57958d12e2409e65", size = 682037, upload-time = "2026-09-27T05:21:21.707Z" }, + { url = "https://files.pythonhosted.org/packages/2b/72/6c87e74d5763512fcdd4da92654c611529ddead00b5eafe7265640cd3b8f/hypothesis-6.168.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:20b5024b215208847e5bd2003a89110b74031bfa218d05b10edeb8b8c1354c42", size = 791861, upload-time = "2026-09-27T05:22:03.303Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/d6df42f5fa6867a3aeadf372743f2def8d1c4444ce48a06a0250954c5bec/hypothesis-6.168.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7d052c2770ba607e00f7a40965021ceefcd676ee459a1e44daa26ed61d56f32", size = 783409, upload-time = "2026-09-27T05:22:17.92Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e0/92fc641a751af530859023ea6ae6f73c2294587240b78884aa155fccbe5a/hypothesis-6.168.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798ae9a91ab4cd8cfadd7991f46dc9c12fd43d2997f29744b7eae755ff36027a", size = 1121651, upload-time = "2026-09-27T05:21:40.104Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/405c4c9b888721fc3b650ffc3c86d8507450e1ea105e88c97e2e5f277aa6/hypothesis-6.168.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00f8728aaef10c91688dab34df8a59aed83c76ade869b1812f01ac80808bc5cc", size = 1167748, upload-time = "2026-09-27T05:21:41.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/70/2f7559b159f98e14d950914a0aeb25b0e2ed473f35705192de4a2c20b3e8/hypothesis-6.168.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:63b183d5436688e54389f54b66d1611aef349f36345b6dc0a79ccef0dbc736c8", size = 1297293, upload-time = "2026-09-27T05:21:03.359Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c3/b197f8353c3026b76da82d49a119849cd9eac7b0be96239414d1d748156c/hypothesis-6.168.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:28dfe52bcf6e6621153cfbb5574223d117a3e66b24ffeacb4c9bc8f751190dc6", size = 1334246, upload-time = "2026-09-27T05:21:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/555178285b606bad3e1127868500b064822574e409618f0e88c17cb63242/hypothesis-6.168.2-cp314-cp314t-win_amd64.whl", hash = "sha256:4aa407c3b0b1d7d980088e6aeb634a53cca879d68ffaf1efdb284b2fc70441e4", size = 681980, upload-time = "2026-09-27T05:21:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/30c6ca63dfe1c372670df30f7fd0f42efe890c13def205c9c4ca48eea716/hypothesis-6.168.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:567ced8b7eca653544a70f0e44c381a6cbe151c6a6544119e39fb9d2f43c3b4c", size = 791262, upload-time = "2026-09-27T05:21:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/87/3f/373a33a0e85fc74fb2a525023a4d163b29ca5ff60f6cc106ca205324dffe/hypothesis-6.168.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:c1fb42a01f8b6d235887787772d6969453440d3d4a6bf52be6831a720f4caf25", size = 783152, upload-time = "2026-09-27T05:21:45.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/98/55f83a1a9cb643866c27211a991cf56a012126a6d5548a02877df1334ab0/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3001d9e6013ebb82d9d7daa97efd75a383e628bf2b5d754512b56191898e377", size = 1121134, upload-time = "2026-09-27T05:21:32.576Z" }, + { url = "https://files.pythonhosted.org/packages/87/dd/2a642db35341893ee604d8cd0b8e6232ddc848e9fe04d6a17fd30cdbc586/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44b7c3d431cca39342881761dfe3e34b39c9b5e7b0ad5c8c4cb7d92514f3b94c", size = 1144046, upload-time = "2026-09-27T05:20:06.616Z" }, + { url = "https://files.pythonhosted.org/packages/61/13/78e7b3192d4ccd19bbf36ebc0a678a8291f40093baee47fdded4240d2ca7/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78ce758211e2fd049c358c979c0c1312fc0f6e13a829cf2a4828a9ebb1423f8", size = 1146678, upload-time = "2026-09-27T05:21:34.447Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6a/daa33a3e9eb999da96bd7315d1f5446c5e28866191e2007aad9b1fb6616f/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9aec441b5d7156e64285817fda72b0aad37aa3dd262693708e825858930b7b48", size = 1189433, upload-time = "2026-09-27T05:21:55.422Z" }, + { url = "https://files.pythonhosted.org/packages/28/49/138fa2c61ec12ea7b17a1693a17ce0567ede4e61dac65d3304664493641b/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3510ca51dfd5aa7eb24f39d65714f6a91297fa5382866124b931bf56acd207fd", size = 1167045, upload-time = "2026-09-27T05:22:13.561Z" }, + { url = "https://files.pythonhosted.org/packages/e5/04/38474d9c6bc119700b6eb6fdb73ff8775202af79d5a7ee3bd06619d1b5a0/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d7943a4c8dcca314bfd918247acea4a36fd3476167372b19d8db3070cdefa4f", size = 1126814, upload-time = "2026-09-27T05:21:43.835Z" }, + { url = "https://files.pythonhosted.org/packages/99/be/048888e2f1bfe412c46788443079bb03b6c299c0da85f11fdb536f27dfa4/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:903cc76f9ebd36d555039aac270f5652cf0a93cfbbac344915ee3529e917efa3", size = 1155839, upload-time = "2026-09-27T05:20:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/0a607ac12fedbd8b2be26b360013df3b064e6b80d7313db635bfb5cc75de/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d6f060f4bcf10ac087c34da08fc9f3be979dcfb70d45a238a6fe6298f1cc1ae9", size = 1296640, upload-time = "2026-09-27T05:21:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f8/f59636b8b9b8c6de5f02b752faba3c7592a13dd767ddc96bf582b8870720/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8574573eea6693792966eb6c8da079fe63d8dafb884c613c9857b3a13052df45", size = 1422025, upload-time = "2026-09-27T05:20:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/e36c4a95b7997e213cd85c3be1232c8ac19e1ecf20d99512ea279eade321/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:5714ac1e72ba5faa645f593de353af34a8b1de8bcff811f1d3da4c1b8628e1c3", size = 1374166, upload-time = "2026-09-27T05:21:01.762Z" }, + { url = "https://files.pythonhosted.org/packages/18/9e/b9c3e282e9833e75875bc9163f80c49f8153d0f96ce5ba034a7997f13850/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:dffa4c97f0b473f1da456807ff57b348fb675f7728a15faab4eea87dffb6c77a", size = 1278387, upload-time = "2026-09-27T05:20:05.125Z" }, + { url = "https://files.pythonhosted.org/packages/af/96/84ed26fd0855ad8d6d67413893c835e90a77294c1255e3e78c13fe082f0f/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:29c1d761ef45e063eba2f8287e0572976f6b758b6832d75d7e03d02a610d3a91", size = 1297784, upload-time = "2026-09-27T05:20:28.101Z" }, + { url = "https://files.pythonhosted.org/packages/94/19/c94f1bce7e72950f8f3c5ee9d34ba98a7da70548453a96befa86281bfd94/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c7ab9ecf409143ef26a0d20a5f373d2add8871be58028cdbd3a515b0ef43ac45", size = 1333959, upload-time = "2026-09-27T05:21:16.771Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/f2e4bedd7031fb46b35ed9f95841074d990b504c969af6ad18f91e989bdf/hypothesis-6.168.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:bc15fff253b43cbbbe45dafa5773fbf122eeeb9449098ec3cfcdaf9e2aae234d", size = 675380, upload-time = "2026-09-27T05:20:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c97684c2698ef16032c99506308ee96270770addb640d2b154f1bc77a26f/hypothesis-6.168.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:67d5d6f273e51c9832d637af5d45c8d95cfced1f93760ce2056f9886c61c5f7e", size = 681683, upload-time = "2026-09-27T05:21:59.433Z" }, + { url = "https://files.pythonhosted.org/packages/25/85/e3c41febc99d939660f5c26d160f9992662d7cf99c62e60bd4c2419c36f7/hypothesis-6.168.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:3c91cde9298b2da2a2e508c752b1531ed26ecd66b67b6a1af75d7b870bc92fff", size = 679372, upload-time = "2026-09-27T05:20:24.066Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dc/d765a012e2e31dd38f899487ec28d4bc33eb5cbf16ca4f90023005f5a0de/hypothesis-6.168.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ff8453251976d8090b323eede58dde1b77f99404a3c4c2d0eebf5bfedbd34d94", size = 793121, upload-time = "2026-09-27T05:21:25.179Z" }, + { url = "https://files.pythonhosted.org/packages/72/fa/616344dcf5e4ec9952dde2d8b81ff6dc4652ea3efb5511ba8b4e1a36273f/hypothesis-6.168.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:58e471bb09351a1c12f1a5da053f43d23dc359cddb3fd9c0d56ef6f68649751e", size = 788974, upload-time = "2026-09-27T05:21:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/f9/02/0a5d02cfc75194a410e46763463d8b8cf1f51437633ffcf7f345f556dccc/hypothesis-6.168.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ad8f0e1f1b32f293ead803bc16ce20066414b14f36d594d170119520ccc3e8", size = 1124963, upload-time = "2026-09-27T05:20:26.494Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d5/99044493d2bda5a6213dc0df7ffd52b91447bfa4817031a751299887a408/hypothesis-6.168.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b70add4949a4be468b5461e01b125d42d58ad03f92da79261e9aab25eff9002", size = 1171944, upload-time = "2026-09-27T05:20:48.285Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/8ef748e3f55bdba4a04b805bdc29c4462b2e5280900f0dc9347a0f0167cb/hypothesis-6.168.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d53ff2916fded1ab8b8abe83c33e8cafb4f9a8cabeca114a037bdb8bd715f2df", size = 685625, upload-time = "2026-09-27T05:21:51.479Z" }, ] [[package]] From 69c62ff376840c8b14440b34b7a9a96f39930adb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:05:22 +0200 Subject: [PATCH 120/322] chore: Bump the all-npm group with 2 updates (#5018) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 216 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90a8072726..df6adcd312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.6.2", + "@types/node": "^26.6.3", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.4", + "rollup": "^4.63.5", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", - "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", - "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", - "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", - "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", - "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", - "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", - "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", - "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", - "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", - "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", - "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", - "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", - "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", - "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", - "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", - "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", - "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", - "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", - "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", - "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", - "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", - "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", - "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", - "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", - "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", - "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", - "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.4", - "@rollup/rollup-android-arm64": "4.63.4", - "@rollup/rollup-darwin-arm64": "4.63.4", - "@rollup/rollup-darwin-x64": "4.63.4", - "@rollup/rollup-freebsd-arm64": "4.63.4", - "@rollup/rollup-freebsd-x64": "4.63.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", - "@rollup/rollup-linux-arm-musleabihf": "4.63.4", - "@rollup/rollup-linux-arm64-gnu": "4.63.4", - "@rollup/rollup-linux-arm64-musl": "4.63.4", - "@rollup/rollup-linux-loong64-gnu": "4.63.4", - "@rollup/rollup-linux-loong64-musl": "4.63.4", - "@rollup/rollup-linux-ppc64-gnu": "4.63.4", - "@rollup/rollup-linux-ppc64-musl": "4.63.4", - "@rollup/rollup-linux-riscv64-gnu": "4.63.4", - "@rollup/rollup-linux-riscv64-musl": "4.63.4", - "@rollup/rollup-linux-s390x-gnu": "4.63.4", - "@rollup/rollup-linux-x64-gnu": "4.63.4", - "@rollup/rollup-linux-x64-musl": "4.63.4", - "@rollup/rollup-openbsd-x64": "4.63.4", - "@rollup/rollup-openharmony-arm64": "4.63.4", - "@rollup/rollup-win32-arm64-msvc": "4.63.4", - "@rollup/rollup-win32-ia32-msvc": "4.63.4", - "@rollup/rollup-win32-x64-gnu": "4.63.4", - "@rollup/rollup-win32-x64-msvc": "4.63.4", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 819149a3bf..60e3006d74 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.6.2", + "@types/node": "^26.6.3", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.4", + "rollup": "^4.63.5", "terser": "^5.51.2", "typescript": "^7.0.2" }, From 3509e0996fba5adf814a85dcc933b02d12b95576 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:05:49 +0200 Subject: [PATCH 121/322] chore: Bump brace-expansion from 5.0.9 to 5.0.12 (#5023) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index df6adcd312..e2801097a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -910,9 +910,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" From 79bed98dc175494e8ac19973c7c384303ded5c6d Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 18:35:45 +0200 Subject: [PATCH 122/322] fix(js/python/dart): handle Rune in non-Rust builtin matches (#5024) --- src/Fable.Transforms/Dart/Replacements.fs | 2 ++ src/Fable.Transforms/Python/Replacements.fs | 2 ++ src/Fable.Transforms/Replacements.fs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 7bb4101188..751d073d1c 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -50,6 +50,7 @@ let coreModFor = | FSharpReference _ -> "Types" | BclHashSet _ -> "MutableSet" | BclDictionary _ -> "MutableMap" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise let makeLongInt com r t signed (x: uint64) = @@ -4223,6 +4224,7 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclDateTime -> Some(Types.datetime, dates, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 04d7f63c01..4d78c01fa2 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -90,6 +90,7 @@ let coreModFor = | BclDictionary _ -> "mutable_map" | BclDateOnly -> "date_only" | BclTimeOnly -> "time_only" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise let makeDecimal com r t (x: decimal) = @@ -4380,6 +4381,7 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclDateTime -> Some(Types.datetime, dates, []) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index d6a984ba28..fd3de104f4 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -94,6 +94,7 @@ let coreModFor = | FSharpReference _ -> "Types" | BclHashSet _ -> "MutableSet" | BclDictionary _ -> "MutableMap" + | BclRune | BclKeyValuePair _ -> FableError "Cannot decide core module" |> raise let dateOnlyModule (com: Compiler) = @@ -4969,6 +4970,7 @@ let tryType typ = | List genArg -> Some(Types.list, lists, [ genArg ]) | Builtin kind -> match kind with + | BclRune -> None | BclGuid -> Some(Types.guid, guids, []) | BclDateTime -> Some(Types.datetime, dateTime, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dateTimeOffset, []) From 963258948a36119df1575493c45c9bc613d3a21f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 19:50:09 +0200 Subject: [PATCH 123/322] fix(python): import union cases for reflection (#5028) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Python/Fable2Python.Reflection.fs | 24 +------- .../Python/Fable2Python.Transforms.fs | 55 ------------------- .../Python/Fable2Python.Util.fs | 47 ++++++++++++++++ tests/Python/Fable.Tests.Python.fsproj | 1 + tests/Python/TestReflectionUnionImports.fs | 19 +++++++ 5 files changed, 69 insertions(+), 77 deletions(-) create mode 100644 tests/Python/TestReflectionUnionImports.fs diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index c28508fd91..0c45731748 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -104,30 +104,9 @@ let private transformUnionReflectionInfo com ctx r (ent: Fable.Entity) generics let py, stmts = pyConstructor com ctx ent - // Generate case constructors list for make_union - // Use full case class names (UnionName_CaseName) to match the generated classes, - // except for library types (Result, Choice) which use simple names - let usesSimpleNames = Util.usesSimpleCaseNames ent.FullName - - // Get the entity declaration name (with module scope) for consistent naming - let entityDeclName = FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - let caseConstructors = ent.UnionCases - |> Seq.map (fun uci -> - let caseName = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - - let caseClassName = - if usesSimpleNames then - caseName - else - $"%s{entityDeclName}_%s{caseName}" - - com.GetIdentifierAsExpr(ctx, caseClassName) - ) + |> Seq.map (Util.getUnionCaseRef com ctx ent.Ref ent) |> Seq.toList |> Expression.list @@ -237,6 +216,7 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map genericEntity fullName [], [] | Replacements.Util.BclHashSet gen | Replacements.Util.FSharpSet gen -> diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 5bde8afa1f..cfbe4a9ebd 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -111,65 +111,10 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. args, body, returnType -let getUnionCaseName (uci: Fable.UnionCase) = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - /// Attribute holding the precomputed singleton instance of a zero-field union case (see transformUnion). let unionCaseSingletonAttrName = "singleton" let unionCaseSingletonAttr = Identifier unionCaseSingletonAttrName -/// Gets the unique case class name by prefixing with the union type name. -/// This prevents collisions when different union types have cases with the same name. -/// Library types (Result, Choice) use simple case names without prefix. -/// The optional entityName parameter should be the compiled entity name (with module scope). -let getUnionCaseClassName - (com: IPythonCompiler) - (ent: Fable.Entity) - (uci: Fable.UnionCase) - (entityName: string option) - = - let caseName = getUnionCaseName uci - // Library types use simple names (Ok, Error, Choice1Of2, etc.) for backwards compatibility - if usesSimpleCaseNames ent.FullName then - caseName - else - // Use provided entity name or compute from entity reference - let unionName = - match entityName with - | Some name -> name - | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - - $"%s{unionName}_%s{caseName}" - -/// Resolves the expression referring to a union case's class (constructor / type), -/// handling both library types (Result, Choice - simple names from fable_library) -/// and user-defined unions (full case class name, imported from another module if needed). -let getUnionCaseRef (com: IPythonCompiler) ctx (entRef: Fable.EntityRef) (ent: Fable.Entity) (uci: Fable.UnionCase) = - if isLibraryUnionType entRef.FullName then - let caseName = getUnionCaseName uci - // Result uses "result" module, Choice uses "choice" module - let moduleName = - if entRef.FullName = Types.result then - "result" - else - "choice" - - libValue com ctx moduleName caseName - else - // User-defined union - use full case class name (UnionName_CaseName) - let caseClassName = getUnionCaseClassName com ent uci None - - match entRef.SourcePath with - | Some path when path <> com.CurrentFile -> - // Import from another module - let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path - com.GetImportExpr(ctx, importPath, caseClassName) - | _ -> - // Local - just get identifier - com.GetIdentifierAsExpr(ctx, caseClassName) - let getUnionExprTag (com: IPythonCompiler) ctx r (fableExpr: Fable.Expr) = Expression.withStmts { let! expr = com.TransformAsExpr(ctx, fableExpr) diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 51a854e994..e449751b98 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -53,6 +53,53 @@ module Util = | "FSharp.Core.FSharpResult`2" -> true | _ -> false + let getUnionCaseName (uci: Fable.UnionCase) = + match uci.CompiledName with + | Some cname -> cname + | None -> uci.Name + + let getUnionCaseClassName + (com: IPythonCompiler) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + (entityName: string option) + = + let caseName = getUnionCaseName uci + + if usesSimpleCaseNames ent.FullName then + caseName + else + let unionName = + match entityName with + | Some name -> name + | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref + + $"%s{unionName}_%s{caseName}" + + let getUnionCaseRef + (com: IPythonCompiler) + ctx + (entRef: Fable.EntityRef) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + = + if isLibraryUnionType entRef.FullName then + let moduleName = + if entRef.FullName = Types.result then + "result" + else + "choice" + + libValue com ctx moduleName (getUnionCaseName uci) + else + let caseClassName = getUnionCaseClassName com ent uci None + + match entRef.SourcePath with + | Some path when path <> com.CurrentFile -> + let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path + com.GetImportExpr(ctx, importPath, caseClassName) + | _ -> com.GetIdentifierAsExpr(ctx, caseClassName) + /// Ensures a statement list is non-empty by adding Pass if needed. /// Python requires at least one statement in function/class/match bodies. let ensureNonEmptyBody stmts = diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 7cc7ffee62..24ef0fbcf9 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -63,6 +63,7 @@ + diff --git a/tests/Python/TestReflectionUnionImports.fs b/tests/Python/TestReflectionUnionImports.fs new file mode 100644 index 0000000000..1934a4a7e0 --- /dev/null +++ b/tests/Python/TestReflectionUnionImports.fs @@ -0,0 +1,19 @@ +module Fable.Tests.ReflectionUnionImports + +open FSharp.Reflection +open Util.Testing + +type RecordWithResult = { Value: Result } + +let getGenericTypeArguments (typ: System.Type) = typ.GenericTypeArguments + +[] +let ``test FSharp.Reflection: record field Result imports its case constructors`` () = + let field = FSharpType.GetRecordFields(typeof).[0] + field.PropertyType |> equal typeof> + +[] +let ``test FSharp.Reflection: Choice imports its case constructors`` () = + getGenericTypeArguments typeof> + |> Array.length + |> equal 2 From ff71a8c7c87a5333ca0320a083a46c2fb7302029 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 19:50:49 +0200 Subject: [PATCH 124/322] fix(python): avoid name collisions in nested tuple matches (#5027) --- src/Fable.Transforms/FSharp2Fable.Util.fs | 7 ++++ tests/Python/TestPatternMatch.fs | 50 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 61eb69055e..13d68c3871 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -810,6 +810,13 @@ module Helpers = || ctx.UsedNamesInDeclarationScope.Contains name let getIdentUniqueName (com: Compiler) (ctx: Context) name = + // Reserve the emitted Python name so tuple-match temporaries cannot shadow + // existing bindings when camelCase is converted to snake_case. + let name = + match com.Options.Language with + | Python -> Fable.Py.Naming.toPythonNaming name + | _ -> name + let sanitizeIdent = match com.Options.Language with | Python -> Fable.Py.Naming.sanitizeIdent diff --git a/tests/Python/TestPatternMatch.fs b/tests/Python/TestPatternMatch.fs index 932d0eb987..0095333d90 100644 --- a/tests/Python/TestPatternMatch.fs +++ b/tests/Python/TestPatternMatch.fs @@ -311,6 +311,56 @@ let ``test nested match complex`` () = nestedComplex 2 2 |> equal "x=2, y=other" nestedComplex 3 1 |> equal "x=other" +// https://github.com/fable-compiler/Fable/issues/5026 +module NestedMatchNames = + type Shape = + | Circle of radius: int + | Square + + let describe (shapes: Shape list) n = + match List.head shapes with + | Circle radius -> + match n + 1, radius with + | 2, 1 -> "unit circle" + | _ -> "other" + | Square -> "square" + + let describeInClosure (shapes: Shape list) = + match List.head shapes with + | Circle radius -> + fun xs -> + match List.tryHead xs, radius with + | Some 2, 1 -> "unit circle" + | _ -> "other" + | Square -> fun _ -> "square" + + let describeWithUserBinding (match_value: string) n = + match n + 1, n + 2 with + | 2, 3 -> match_value + | _ -> "other" + +[] +let ``test nested tuple match preserves outer union fields`` () = + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 1 |> equal "unit circle" + NestedMatchNames.describe [ NestedMatchNames.Circle 2 ] 1 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Circle 1 ] 2 |> equal "other" + NestedMatchNames.describe [ NestedMatchNames.Square ] 1 |> equal "square" + +[] +let ``test tuple match in closure preserves captured union fields`` () = + let describe = NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 1 ] + describe [ 2 ] |> equal "unit circle" + describe [ 3 ] |> equal "other" + describe [] |> equal "other" + describe [ 2 ] |> equal "unit circle" + NestedMatchNames.describeInClosure [ NestedMatchNames.Circle 2 ] [ 2 ] |> equal "other" + NestedMatchNames.describeInClosure [ NestedMatchNames.Square ] [ 2 ] |> equal "square" + +[] +let ``test tuple match temporaries preserve user bindings`` () = + NestedMatchNames.describeWithUserBinding "matched" 1 |> equal "matched" + NestedMatchNames.describeWithUserBinding "matched" 2 |> equal "other" + // ---------------------------------------------------------------------------- // 7. Option Matching // ---------------------------------------------------------------------------- From f3c0e802f3d13abfc76172fcfa4a62a47c0c6caa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:41:12 +0200 Subject: [PATCH 125/322] chore: release multiple projects (#4996) --- src/Fable.Cli/CHANGELOG.md | 33 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 33 ++++++++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 32 +++++++++++++++++++++++- src/fable-compiler-js/package.json | 4 +-- src/fable-standalone/CHANGELOG.md | 32 +++++++++++++++++++++++- src/fable-standalone/package.json | 4 +-- 7 files changed, 131 insertions(+), 9 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index aa9ea9c7ca..ac1096f479 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.19.0 - 2026-09-30 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) + ## 5.18.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 052f94584d..a8cef75097 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.18.0 - 2026-09-30 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) + ## 5.17.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 7b05d173c2..83e471390a 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.18.0" + let VERSION = "5.19.0" [] let JS_LIBRARY_VERSION = "2.8.0" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index e3b2d7f139..06b8d1b53c 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.6.0 - 2026-09-30 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) + ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 178671e52f..b1b9be26d8 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.5.0", + "version": "3.6.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.5.0" + "@fable-org/fable-standalone": "^3.6.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 7ae000f9f8..4816e0ee12 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.6.0 - 2026-09-30 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) + ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 8cf452f77e..d9b708520b 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.5.0", - "fableVersion": "5.18.0", + "version": "3.6.0", + "fableVersion": "5.19.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:11:04 -0700 Subject: [PATCH 126/322] fix(all): Minor type const strings cleanup (#5029) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 2 ++ src/Fable.Transforms/Beam/Replacements.fs | 2 +- src/Fable.Transforms/Dart/Fable2Dart.fs | 4 ++-- src/Fable.Transforms/Dart/Replacements.fs | 10 +++++----- src/Fable.Transforms/Python/Replacements.fs | 6 +++--- src/Fable.Transforms/Replacements.Util.fs | 2 +- src/Fable.Transforms/Replacements.fs | 4 ++-- src/Fable.Transforms/Rust/Fable2Rust.fs | 2 +- src/Fable.Transforms/Rust/Replacements.fs | 6 +++--- src/fable-library-rust/src/print_no_std.rs | 2 +- 10 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 1b62340a4f..89ad79c781 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -363,6 +363,7 @@ module Reflection = | Replacements.Util.BclDateTimeOffset | Replacements.Util.BclDateOnly | Replacements.Util.BclTimeOnly + | Replacements.Util.BclRune | Replacements.Util.BclTimer -> genericEntity fullName [||] | Replacements.Util.BclHashSet gen | Replacements.Util.FSharpSet gen -> @@ -774,6 +775,7 @@ module Annotation = makeFableLibImportTypeAnnotation com ctx [] "TimeOnlyTemporal" "PlainTime" else NumberTypeAnnotation + | Replacements.Util.BclRune -> NumberTypeAnnotation | Replacements.Util.BclTimer -> makeFableLibImportTypeAnnotation com ctx [] "Timer" "Timer" | Replacements.Util.BclHashSet key -> makeFableLibImportTypeAnnotation com ctx [ key ] "Util" "ISet" | Replacements.Util.BclDictionary(key, value) -> diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index c88a91dcb1..5ea619bb6f 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -6044,7 +6044,7 @@ let tryCall | "System.Diagnostics.Stopwatch" -> stopwatch com ctx r t info thisArg args | Types.nullable -> nullables com ctx r t info thisArg args | Types.guid -> guids com ctx r t info thisArg args - | "System.Random" -> randoms com ctx r t info thisArg args + | Types.random -> randoms com ctx r t info thisArg args | "System.Environment" -> match info.CompiledName with | "get_NewLine" -> makeStrConst "\n" |> Some diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index 658947e807..f5e9570c08 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -221,7 +221,7 @@ module Util = match entRef.FullName, genArgs with | Types.enum_, _ - | "System.Text.Rune", _ -> Integer + | Types.rune, _ -> Integer // F# Quotation types are modelled dynamically by the quotation runtime, // so map them to `dynamic` (the runtime returns plain tagged objects). | Types.fsharpExpr, _ @@ -243,7 +243,7 @@ module Util = | "Fable.Core.Dart.DartNullable`1", [ genArg ] -> Nullable genArg | Types.regexGroup, _ -> Nullable String | Types.regexMatch, _ -> makeTypeRefFromName "Match" [] - | "System.Random", _ -> libTypeRef com ctx "Random" "Random" [] + | Types.random, _ -> libTypeRef com ctx "Random" "Random" [] // We use `dynamic` for Exception because there is no single type that catches all errors in Dart // (except when inherited as base class, then it's getExceptionType, see transformInheritedClass) | Types.exception_, _ -> Dynamic // getExceptionType com ctx diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 751d073d1c..4766a44f1a 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -248,7 +248,7 @@ let toString com (ctx: Context) r (args: Expr list) = match head.Type with | String -> head | Char -> charToString head - | DeclaredType({ FullName = "System.Text.Rune" }, _) -> charToString head + | DeclaredType({ FullName = Types.rune }, _) -> charToString head // | Builtin BclGuid when tail.IsEmpty -> head // | Builtin (BclGuid|BclTimeSpan|BclTimeOnly|BclDateOnly as bt) -> // Helper.LibCall(com, coreModFor bt, "toString", String, args) @@ -3966,7 +3966,7 @@ let private replacedModules = "System.Runtime.ExceptionServices.ExceptionDispatchInfo", exceptionDispatchInfo Types.char, chars Types.string, strings - "System.Text.Rune", runes + Types.rune, runes "Microsoft.FSharp.Core.StringModule", stringModule "System.FormattableString", formattableString "System.Runtime.CompilerServices.FormattableStringFactory", formattableString @@ -4051,10 +4051,10 @@ let private replacedModules = Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.CancellationTokenRegistration", disposables @@ -4230,7 +4230,7 @@ let tryType typ = | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 4d78c01fa2..2f18e27862 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -4218,14 +4218,14 @@ let private replacedModules = Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization "System.IO.File", files "System.IO.Directory", directories "System.IO.Path", paths "System.IO.TextWriter", textWriter - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor @@ -4386,7 +4386,7 @@ let tryType typ = | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclDateTime -> Some(Types.datetime, dates, []) | BclDateTimeOffset -> Some(Types.datetimeOffset, dates, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) | BclKeyValuePair(key, value) -> Some(Types.keyValuePair, keyValuePairs, [ key; value ]) diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index fe3b8ddf50..30009f5914 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -315,7 +315,7 @@ let (|BuiltinDefinition|_|) = | Types.dateOnly -> ValueSome BclDateOnly | Types.timeOnly -> ValueSome BclTimeOnly | Types.rune -> ValueSome BclRune - | "System.Timers.Timer" -> ValueSome BclTimer + | Types.timer -> ValueSome BclTimer | Types.fsharpSet -> ValueSome(FSharpSet(Any)) | Types.fsharpMap -> ValueSome(FSharpMap(Any, Any)) | Types.hashset -> ValueSome(BclHashSet(Any)) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index fd3de104f4..aec37196c9 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -4784,14 +4784,14 @@ let private replacedModules = Types.dateOnly, dateOnly Types.timeOnly, timeOnly Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.IO.File", files "System.IO.Directory", directories "System.IO.Path", paths "System.IO.TextWriter", textWriter "System.Environment", systemEnv "System.Globalization.CultureInfo", globalization - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 9d9f4ae429..2b557b0bb3 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1032,7 +1032,7 @@ module TypeInfo = mkEmitTy value genArgs | ent when ent.IsInterface -> transformInterfaceType com ctx entRef genArgs | ent when ent.IsAbstractClass -> transformAbstractClassType com ctx entRef genArgs - | _ when entRef.FullName = "System.Random" -> + | _ when entRef.FullName = Types.random -> let entName = getLibraryImportName com ctx "Random" "Random" let genArgsOpt = transformGenArgs com ctx genArgs makeFullNamePathTy entName genArgsOpt diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index c459022ef5..c242b41b0d 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -3509,12 +3509,12 @@ let private replacedModules = Types.timeOnly, timeOnly Types.rune, runes Types.timespan, timeSpans - "System.Timers.Timer", timers + Types.timer, timers "System.Environment", systemEnv "System.IO.File", files "System.IO.Path", paths Types.cultureInfo, globalization - "System.Random", random + Types.random, random "System.Threading.CancellationToken", cancels "System.Threading.CancellationTokenSource", cancels "System.Threading.Monitor", monitor @@ -3674,7 +3674,7 @@ let tryType typ = | BclDateOnly -> Some(Types.dateOnly, dateOnly, []) | BclTimeOnly -> Some(Types.timeOnly, timeOnly, []) | BclRune -> Some(Types.rune, runes, []) - | BclTimer -> Some("System.Timers.Timer", timers, []) + | BclTimer -> Some(Types.timer, timers, []) | BclTimeSpan -> Some(Types.timespan, timeSpans, []) | BclHashSet genArg -> Some(Types.hashset, hashSets, [ genArg ]) | BclDictionary(key, value) -> Some(Types.dictionary, dictionaries, [ key; value ]) diff --git a/src/fable-library-rust/src/print_no_std.rs b/src/fable-library-rust/src/print_no_std.rs index 63aa6356b2..f5f83facb6 100644 --- a/src/fable-library-rust/src/print_no_std.rs +++ b/src/fable-library-rust/src/print_no_std.rs @@ -4,7 +4,7 @@ const STDERR: i32 = 2; #[link(name = "c")] unsafe extern "C" { - fn write(filedes: i32, buf: *const core::ffi::c_void, nbyte: usize); + fn write(filedes: core::ffi::c_int, buf: *const core::ffi::c_void, nbyte: usize) -> isize; } pub fn write_to(filedes: i32, s: &str) { From e6e9f54c905750a1bf78ec454bca7d83cdffb61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 3 Oct 2026 01:48:19 -0700 Subject: [PATCH 127/322] fix(rust): Remove trailing comma from macro args (#5031) --- .../Rust/AST/Rust.AST.Helpers.fs | 9 +++++---- src/fable-library-rust/src/Event.rs | 6 +++--- src/fable-library-rust/src/Native.rs | 18 +++++++++--------- src/fable-library-rust/src/Observable.rs | 6 +++--- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs index cd7f4e8550..2fafbb99ee 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs @@ -600,10 +600,11 @@ module MacroArgs = |> Seq.mapi (fun i tok -> let ttt = tok |> token.TokenTree.Token let sep = kind |> mkTokenTree - // if i < count - 1 then - [ (ttt, token.Spacing.Joint); (sep, token.Spacing.Alone) ] - // else - // [ (ttt, token.Spacing.Alone) ] + + if i < count - 1 then + [ (ttt, token.Spacing.Joint); (sep, token.Spacing.Alone) ] + else + [ (ttt, token.Spacing.Alone) ] ) |> Seq.concat |> mkVec diff --git a/src/fable-library-rust/src/Event.rs b/src/fable-library-rust/src/Event.rs index a197032210..865aef440e 100644 --- a/src/fable-library-rust/src/Event.rs +++ b/src/fable-library-rust/src/Event.rs @@ -118,7 +118,7 @@ pub mod Event_ { ) -> LrcPtr> { interface_cast!( LrcPtr::new(EventObservable { source }), - Lrc>, + Lrc> ) } @@ -174,7 +174,7 @@ pub mod Event_ { let publish = interface_cast!(LrcPtr::new(AnonymousEvent { add_handler, remove_handler, - }), Lrc, T>>,); + }), Lrc, T>>); LrcPtr::new(FSharpEvent_1 { handlers, Publish: publish, @@ -239,7 +239,7 @@ pub mod Event_ { let publish = interface_cast!(LrcPtr::new(AnonymousEvent { add_handler, remove_handler, - }), Lrc, T>>,); + }), Lrc, T>>); LrcPtr::new(FSharpEvent_2 { handlers, Publish: publish }) } diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index 105b1cc0ab..c3ea70f006 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -432,7 +432,7 @@ pub mod Native_ { { interface_cast!( EqualityComparer_1::::get_Default(), - Lrc>, + Lrc> ) } @@ -595,7 +595,7 @@ pub mod Native_ { #[cfg(not(feature = "lrc_ptr"))] #[macro_export] macro_rules! interface_cast { - ($value:expr, $ifc:ty,) => { + ($value:expr, $ifc:ty) => { ($value as $ifc) }; } @@ -603,7 +603,7 @@ pub mod Native_ { #[cfg(feature = "lrc_ptr")] #[macro_export] macro_rules! interface_cast { - ($value:expr, $ifc:ty,) => { + ($value:expr, $ifc:ty) => { LrcPtr::from((*$value).clone() as $ifc) }; } @@ -637,8 +637,8 @@ pub mod Native_ { #[macro_export] macro_rules! un_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $($args:ty),*) => { - impl<$($args),*> core::ops::$op_trait for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty $(,$args:ty)*) => { + impl<$($args)*> core::ops::$op_trait for $obj { type Output = Self; #[inline] fn $op_fn(self) -> Self::Output { @@ -650,8 +650,8 @@ pub mod Native_ { #[macro_export] macro_rules! bin_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty, $($args:ty,)*) => { - impl<$($args),*> core::ops::$op_trait<$rhs> for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty $(,$args:ty,)*) => { + impl<$($args)*> core::ops::$op_trait<$rhs> for $obj { type Output = Self; #[inline] fn $op_fn(self, rhs: $rhs) -> Self::Output { @@ -663,8 +663,8 @@ pub mod Native_ { #[macro_export] macro_rules! div_int_op { - ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty, $($args:ty,)*) => { - impl<$($args),*> $crate::Native_::DivideByInt for $obj { + ($op_trait:ident, $op_fn:ident, $op:ident, $obj:ty, $rhs:ty $(,$args:ty,)*) => { + impl<$($args)*> $crate::Native_::DivideByInt for $obj { #[inline] fn divide_by_int(self, rhs: i32) -> Self { <$obj>::$op(self, rhs) diff --git a/src/fable-library-rust/src/Observable.rs b/src/fable-library-rust/src/Observable.rs index 9c1db5718d..39fbc7e661 100644 --- a/src/fable-library-rust/src/Observable.rs +++ b/src/fable-library-rust/src/Observable.rs @@ -124,7 +124,7 @@ pub mod Observable_ { on_error, on_completed, }), - Lrc>, + Lrc> ) } @@ -133,12 +133,12 @@ pub mod Observable_ { ) -> LrcPtr> { interface_cast!( LrcPtr::new(Observable { subscribe }), - Lrc>, + Lrc> ) } pub fn mkDisposable(dispose: Func0<()>) -> LrcPtr { - interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc,) + interface_cast!(LrcPtr::new(Disposable { dispose }), Lrc) } // A no-op error handler (errors are propagated by combinators when present, From aaf35f200be5bbc2698abe242f77b8de745e4337 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sat, 3 Oct 2026 13:19:07 +0200 Subject: [PATCH 128/322] chore: revert release multiple projects (#4996) (#5032) --- src/Fable.Cli/CHANGELOG.md | 33 +------------------------ src/Fable.Compiler/CHANGELOG.md | 33 +------------------------ src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 32 +----------------------- src/fable-compiler-js/package.json | 4 +-- src/fable-standalone/CHANGELOG.md | 32 +----------------------- src/fable-standalone/package.json | 4 +-- 7 files changed, 9 insertions(+), 131 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index ac1096f479..aa9ea9c7ca 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,37 +28,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 5.19.0 - 2026-09-30 - -### 🚀 Features - -* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) - -### 🐞 Bug Fixes - -* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) -* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) -* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) -* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) -* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) -* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) -* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) -* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) -* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) -* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) -* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) -* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) -* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) -* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) -* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) -* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) -* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) -* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) -* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) -* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) - -[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) - ## 5.18.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index a8cef75097..052f94584d 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,37 +21,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 5.18.0 - 2026-09-30 - -### 🚀 Features - -* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) - -### 🐞 Bug Fixes - -* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) -* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) -* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) -* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) -* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) -* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) -* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) -* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) -* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) -* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) -* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) -* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) -* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) -* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) -* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) -* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) -* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) -* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) -* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) -* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) - -[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) - ## 5.17.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 83e471390a..7b05d173c2 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.19.0" + let VERSION = "5.18.0" [] let JS_LIBRARY_VERSION = "2.8.0" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 06b8d1b53c..e3b2d7f139 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,36 +16,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 3.6.0 - 2026-09-30 - -### 🚀 Features - -* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) - -### 🐞 Bug Fixes - -* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) -* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) -* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) -* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) -* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) -* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) -* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) -* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) -* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) -* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) -* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) -* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) -* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) -* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) -* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) -* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) -* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) -* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) -* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) - -[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) - ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index b1b9be26d8..178671e52f 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.6.0", + "version": "3.5.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.6.0" + "@fable-org/fable-standalone": "^3.5.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 4816e0ee12..7ae000f9f8 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ff71a8c7c87a5333ca0320a083a46c2fb7302029 +last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,36 +20,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 3.6.0 - 2026-09-30 - -### 🚀 Features - -* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) - -### 🐞 Bug Fixes - -* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) -* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) -* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) -* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) -* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) -* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) -* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) -* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) -* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) -* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) -* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) -* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) -* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) -* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) -* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) -* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) -* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) -* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) -* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) - -[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..ff71a8c7c87a5333ca0320a083a46c2fb7302029) - ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index d9b708520b..8cf452f77e 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.6.0", - "fableVersion": "5.19.0", + "version": "3.5.0", + "fableVersion": "5.18.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 121940803be139b15328b43b153787fde53d4eb8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:07:28 +0200 Subject: [PATCH 129/322] chore: release multiple projects (#5030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🤖 easybuild-shipit --- src/Fable.Cli/CHANGELOG.md | 35 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 35 ++++++++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 34 +++++++++++++++++++++++- src/fable-compiler-js/package.json | 4 +-- src/fable-standalone/CHANGELOG.md | 34 +++++++++++++++++++++++- src/fable-standalone/package.json | 4 +-- 7 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index aa9ea9c7ca..177b0b9b7e 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.19.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + ## 5.18.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 052f94584d..c3730b1d07 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.18.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Use common null storage (#4995) ([ae165dd0](https://github.com/fable-compiler/Fable/commit/ae165dd0cf7248e22c3520d1e00347d0713d0526)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + ## 5.17.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 7b05d173c2..83e471390a 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.18.0" + let VERSION = "5.19.0" [] let JS_LIBRARY_VERSION = "2.8.0" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index e3b2d7f139..9180da009d 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.6.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 178671e52f..b1b9be26d8 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.5.0", + "version": "3.6.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.5.0" + "@fable-org/fable-standalone": "^3.6.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 7ae000f9f8..fe7316460f 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.6.0 - 2026-10-03 + +### 🚀 Features + +* *(dart)* Add support for `System.Text.Rune` (#3860) (#4969) ([389edefb](https://github.com/fable-compiler/Fable/commit/389edefb5ed131ed14fea26a9aa04dde4f5c40cf)) + +### 🐞 Bug Fixes + +* *(all)* Minor type const strings cleanup (#5029) ([9b69e5be](https://github.com/fable-compiler/Fable/commit/9b69e5be8a2cffc3cbcb3920bf9924b72ed385d6)) +* *(beam)* Honor custom ToString in %O (#4994) ([58b79e0a](https://github.com/fable-compiler/Fable/commit/58b79e0a7c03c17aa4aa4981e59621d0559a9a0b)) +* *(beam)* Support AggregateException (#4999) ([e2120e10](https://github.com/fable-compiler/Fable/commit/e2120e1084a259d4080a5943d209b1ddfc400f01)) +* *(beam)* Preserve unmatched filtered exceptions (#5001) ([682b235b](https://github.com/fable-compiler/Fable/commit/682b235bbfdb81e1bccfac7a0ac4680d27448e65)) +* *(beam)* Support integer Log2 (#5002) ([dc964762](https://github.com/fable-compiler/Fable/commit/dc964762b81c0f50de23113bf671050f544037df)) +* *(beam)* Support DivRem ref arguments (#5003) ([902870b1](https://github.com/fable-compiler/Fable/commit/902870b15f46a90ecd034fb40d97e16de4a10713)) +* *(beam)* Range-check integer parsing (#5005) ([d5696304](https://github.com/fable-compiler/Fable/commit/d56963043d6f24296d58fae34b4288cc950315ff)) +* *(beam)* Check System.Convert integer conversions (#5006) ([3ebe4802](https://github.com/fable-compiler/Fable/commit/3ebe480256605e244795e9aa088bbee03347689d)) +* *(beam)* Support Decimal bit constructors (#5007) ([dfecebc5](https://github.com/fable-compiler/Fable/commit/dfecebc59f41195ea75dc1f61bffca3c2d2cbfbf)) +* *(beam)* Share cancellation state across processes (#5010) ([01eeded2](https://github.com/fable-compiler/Fable/commit/01eeded22f292e77c7c6c024de5d894d705fa09d)) +* *(beam)* Close BigInteger API gaps (#5011) ([358548d8](https://github.com/fable-compiler/Fable/commit/358548d8c6d55c719b8bb24cccd2c91b27e4b6d4)) +* *(beam)* Preserve array identity hashing (#5014) ([63a6f12a](https://github.com/fable-compiler/Fable/commit/63a6f12aa742e720d3040423c19707d77092e8bd)) +* *(beam)* Eliminate unsupported expression placeholders (#5013) ([7b3d762d](https://github.com/fable-compiler/Fable/commit/7b3d762debe2b5ab4a15e27a83b211474a7ce3af)) +* *(beam)* Preserve union collection ordering (#5016) ([67b63244](https://github.com/fable-compiler/Fable/commit/67b63244d3eb4f6aefb1a4d0684c4bca4dd9379d)) +* *(js/python/dart)* Handle Rune in non-Rust builtin matches (#5024) ([79bed98d](https://github.com/fable-compiler/Fable/commit/79bed98dc175494e8ac19973c7c384303ded5c6d)) +* *(python)* Import union cases for reflection (#5028) ([96325894](https://github.com/fable-compiler/Fable/commit/963258948a36119df1575493c45c9bc613d3a21f)) +* *(python)* Avoid name collisions in nested tuple matches (#5027) ([ff71a8c7](https://github.com/fable-compiler/Fable/commit/ff71a8c7c87a5333ca0320a083a46c2fb7302029)) +* *(rust)* Add support for System.Text.Rune (#4997) ([bc367c0b](https://github.com/fable-compiler/Fable/commit/bc367c0bde0fa0b52db04d5ee9c52b983fd7ffac)) +* *(rust)* Add missing Decimal members Abs, Sign, CopySign, GetTypeCode, FromOACurrency, ToOACurrency and tests (#4998) ([ec8d4c27](https://github.com/fable-compiler/Fable/commit/ec8d4c270683a6150389d77f7de6c22563c0079d)) +* *(rust)* Add proper UnicodeCategory support (#5009) ([426892ec](https://github.com/fable-compiler/Fable/commit/426892ec00cfd847140e598860cb88a19addb8d9)) +* *(rust)* Remove trailing comma from macro args (#5031) ([e6e9f54c](https://github.com/fable-compiler/Fable/commit/e6e9f54c905750a1bf78ec454bca7d83cdffb61e)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..aaf35f200be5bbc2698abe242f77b8de745e4337) + ## 3.5.0 - 2026-09-25 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 8cf452f77e..d9b708520b 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.5.0", - "fableVersion": "5.18.0", + "version": "3.6.0", + "fableVersion": "5.19.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From dcfaa3d6181f6789d0868e0bf9132ecdcdc76287 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 3 Oct 2026 16:55:23 +0200 Subject: [PATCH 130/322] fix(beam): recognize boxed discriminated unions in type tests (#5033) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 6 + src/Fable.Transforms/Beam/Fable2Beam.Util.fs | 31 +++++ src/Fable.Transforms/Beam/Fable2Beam.fs | 3 +- tests/Beam/UnionTypeTests.fs | 127 +++++++++++++++++++ 4 files changed, 166 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 39ffd753d2..eb0e074d2b 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -405,6 +405,12 @@ Field and case entries retain both source names and emitted Erlang atoms. plus construction and field/case access. Erased runtime values still limit dynamic type tests and formatting where two F# types have the same Erlang shape. +Runtime `:?` tests for ordinary discriminated unions match the compiled case tags +and exact tuple arities; fieldless cases match their bare atoms. These tests accept +arbitrary terms safely and do not inspect payload fields. Generic arguments and +nominal union identity are erased, so types with identical emitted case tags and +arities cannot be distinguished by these tests. + ### Interop | F# | Erlang | diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs index 74f64edefb..9b620dc4a0 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs @@ -9,6 +9,37 @@ open Fable.Beam let toErlangVar (ident: Ident) = Naming.capitalizeFirst ident.Name |> Naming.sanitizeErlangVar +/// Test the existing DU representation without inspecting payload types. +let makeUnionTypeTest (cases: UnionCase list) (expr: Beam.ErlExpr) = + // decision: Match emitted case tags and arities because DU values carry no nominal type or generic argument metadata. + // invariant: Patterns accept arbitrary terms safely and evaluate the tested expression once. + let caseClause (uci: UnionCase) : Beam.ErlCaseClause = + let tag = + Naming.unionCaseTagName uci.CompiledName uci.Name + |> Beam.Atom + |> Beam.ErlLiteral.AtomLit + |> Beam.PLiteral + + let pattern = + match uci.UnionCaseFields with + | [] -> tag + | fields -> Beam.PTuple(tag :: List.replicate fields.Length Beam.PWildcard) + + { + Pattern = pattern + Guard = [] + Body = [ Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit true) ] + } + + let fallback: Beam.ErlCaseClause = + { + Pattern = Beam.PWildcard + Guard = [] + Body = [ Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit false) ] + } + + Beam.ErlExpr.Case(expr, List.map caseClause cases @ [ fallback ]) + let isIntegerType (typ: Fable.AST.Fable.Type) = match typ with | Fable.AST.Fable.Type.Number(kind, _) -> diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 1b18f2be4f..30d9511a16 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -2034,7 +2034,7 @@ and transformTest (com: IBeamCompiler) (ctx: Context) (kind: TestKind) (expr: Ex | Fable.AST.Fable.Type.Tuple _ -> Beam.ErlExpr.Call(None, "is_tuple", [ erlExpr ]) | Fable.AST.Fable.Type.DeclaredType(ref, _) -> // For exception types, check the nominal exn_type tag. - // For other types, check is_map or is_reference (class instances use refs) + // Unions use case tags and arities; other types use maps or references. let isMap = Beam.ErlExpr.Call(None, "is_map", [ erlExpr ]) let isRef = Beam.ErlExpr.Call(None, "is_reference", [ erlExpr ]) @@ -2065,6 +2065,7 @@ and transformTest (com: IBeamCompiler) (ctx: Context) (kind: TestKind) (expr: Ex ) Beam.ErlExpr.BinOp("andalso", isMap, typeCheck) + | _, Some entity when entity.IsFSharpUnion -> makeUnionTypeTest entity.UnionCases erlExpr | _ -> Beam.ErlExpr.BinOp("orelse", isMap, isRef) | _ -> Beam.ErlExpr.Literal(Beam.ErlLiteral.BoolLit true) diff --git a/tests/Beam/UnionTypeTests.fs b/tests/Beam/UnionTypeTests.fs index 0b7f60e3b2..a12f942397 100644 --- a/tests/Beam/UnionTypeTests.fs +++ b/tests/Beam/UnionTypeTests.fs @@ -48,6 +48,133 @@ type WrappedUnion = type T1 = T1 +type ForwardedValue = ForwardedValue of obj + +type GenericForwardedValue<'T> = GenericForwardedValue of 'T + +type ForwardedPayload = { Pid: obj; Reason: obj } + +type RuntimeTaggedUnion = + | [] RuntimeEmpty + | [] RuntimePayload of obj + | [] RuntimePair of obj * obj + +let private unwrapForwardedValue (message: obj) = + match message with + | :? ForwardedValue as forwarded -> + let (ForwardedValue value) = forwarded + Some value + | _ -> None + +let private unwrapGenericForwardedValue<'T> (message: obj) = + match message with + | :? GenericForwardedValue<'T> as forwarded -> + let (GenericForwardedValue value) = forwarded + Some value + | _ -> None + +let private isMyUnion (message: obj) = message :? MyUnion +let private isEither (message: obj) = message :? Either +let private isGender (message: obj) = message :? Gender +let private isT1 (message: obj) = message :? T1 +let private isRuntimeTaggedUnion (message: obj) = message :? RuntimeTaggedUnion + +[] +let ``test boxed single case union type test unwraps payload`` () = + unwrapForwardedValue (box (ForwardedValue (box 42))) |> equal (Some (box 42)) + +[] +let ``test boxed generic union type test unwraps payload`` () = + unwrapGenericForwardedValue (box (GenericForwardedValue 42)) |> equal (Some 42) + unwrapGenericForwardedValue (box (GenericForwardedValue "value")) |> equal (Some "value") + +[] +let ``test boxed multi case union type tests recognize every case`` () = + [ Case0; Case1 "one"; Case2 ("one", "two"); Case3 ("one", "two", "three") ] + |> List.iter (fun value -> isMyUnion (box value) |> equal true) + [ Left 42; Right "value" ] + |> List.iter (fun value -> isEither (box value) |> equal true) + +[] +let ``test boxed fieldless union type tests recognize atoms`` () = + isGender (box Male) |> equal true + isGender (box Female) |> equal true + isT1 (box T1) |> equal true + isT1 (box Male) |> equal false + isGender (box T1) |> equal false + +[] +let ``test boxed union type tests honor compiled case names`` () = + [ RuntimeEmpty; RuntimePayload (box 42); RuntimePair (box 42, null) ] + |> List.iter (fun value -> isRuntimeTaggedUnion (box value) |> equal true) + +[] +let ``test boxed union type test evaluates its input once`` () = + let mutable calls = 0 + let next () = + calls <- calls + 1 + box (RuntimePair (box 42, null)) + (next () :? RuntimeTaggedUnion) |> equal true + calls |> equal 1 + +[] +let ``test boxed union wrappers preserve null and child exit shaped payloads`` () = + unwrapForwardedValue (box (ForwardedValue null)) |> equal (Some null) + unwrapGenericForwardedValue (box (GenericForwardedValue null)) |> equal (Some null) + let payload = { Pid = box 42; Reason = null } + unwrapForwardedValue (box (ForwardedValue (box payload))) |> equal (Some (box payload)) + unwrapGenericForwardedValue (box (GenericForwardedValue payload)) |> equal (Some payload) + unwrapForwardedValue (box payload) |> equal None + unwrapGenericForwardedValue (box payload) |> equal None + +[] +let ``test boxed union type tests reject unrelated values`` () = + [ null; box 42; box true; box "value"; box (ref 42); box [| 42 |] + box {| Pid = 42; Reason = "normal" |}; box (Map.ofList [ "pid", 42 ]) + box (42, "value"); box Male; box T1 ] + |> List.iter (fun value -> + unwrapForwardedValue value |> equal None + unwrapGenericForwardedValue value |> equal None + isMyUnion value |> equal false + isEither value |> equal false + isRuntimeTaggedUnion value |> equal false) + +#if FABLE_COMPILER_BEAM +[ erlang:self(), reason => normal}, erlang:make_ref(), unrelated, {}, forwarded_value, {forwarded_value}, {forwarded_value, 42, extra}, generic_forwarded_value, {generic_forwarded_value}, {generic_forwarded_value, 42, extra}, {other, 42}, {42, forwarded_value}, {case0}, case1, {case1}, {case1, 42, extra}, {case2, 42}, {case2, 42, 43, extra}, {case3, 42, 43}, {case3, 42, 43, 44, extra}, {left}, {left, 42, extra}, {'FORWARDED.Empty'}, 'FORWARDED.Payload', {'FORWARDED.Payload'}, {'FORWARDED.Payload', 42, extra}, {'FORWARDED.Pair', 42}]")>] +let private unrelatedNativeUnionTerms () : obj list = Fable.Core.Util.nativeOnly + +[] +let private compiledNativeUnionTerms () : obj list = Fable.Core.Util.nativeOnly + +[ erlang:self(), reason => normal}")>] +let private nativeChildExitPayload () : ForwardedPayload = Fable.Core.Util.nativeOnly + +[] +let ``test boxed union wrappers preserve native child exit shaped records`` () = + let payload = nativeChildExitPayload () + unwrapForwardedValue (box (ForwardedValue (box payload))) |> equal (Some (box payload)) + unwrapGenericForwardedValue (box (GenericForwardedValue payload)) |> equal (Some payload) + unwrapForwardedValue (box payload) |> equal None + unwrapGenericForwardedValue (box payload) |> equal None + +[] +let ``test boxed union type tests reject malformed native terms safely`` () = + unrelatedNativeUnionTerms () + |> List.iter (fun value -> + unwrapForwardedValue value |> equal None + unwrapGenericForwardedValue value |> equal None + isMyUnion value |> equal false + isEither value |> equal false + isGender value |> equal false + isT1 value |> equal false + isRuntimeTaggedUnion value |> equal false) + +[] +let ``test boxed union type tests recognize native compiled tags`` () = + compiledNativeUnionTerms () + |> List.iter (fun value -> isRuntimeTaggedUnion value |> equal true) +#endif + type CollectionOrder = | Zebra | Alpha From 4ee31586fd88ed0708e218e7f170432baa33551e Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 4 Oct 2026 14:13:14 +0200 Subject: [PATCH 131/322] feat(python): give System exceptions native Python bases (#5040) --- .../Python/Fable2Python.Reflection.fs | 13 ++- src/Fable.Transforms/Python/Replacements.fs | 4 +- .../fable_library/Fable.Library.Python.fsproj | 2 +- src/fable-library-py/fable_library/Native.fs | 32 +++++++ src/fable-library-py/fable_library/System.fs | 96 +++++++++++++++++++ .../fable_library/exception_bases.py | 52 ++++++++++ src/fable-library-py/fable_library/types.py | 17 +--- tests/Python/TestMisc.fs | 88 +++++++++++++++++ 8 files changed, 284 insertions(+), 20 deletions(-) create mode 100644 src/fable-library-py/fable_library/System.fs create mode 100644 src/fable-library-py/fable_library/exception_bases.py diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 0c45731748..477d12facf 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -291,6 +291,17 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map Option.bind (fun baseType -> + let baseEnt = com.GetEntity(baseType.Entity) + + // decision: native exception bridges add Python ancestry without changing .NET reflection ancestry. + match (com :> Compiler), baseEnt.Attributes with + | FSharp2Fable.Util.ImportAtt(_, "fable_library.exception_bases") -> baseEnt.BaseType + | _ -> Some baseType + ) + let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = if ent.IsFSharpRecord then transformRecordReflectionInfo com ctx r ent generics @@ -310,7 +321,7 @@ let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = yield Expression.name (name.Name |> Naming.toPythonNaming), stmts | Some(cons, stmts) -> yield cons, stmts | None -> () - match ent.BaseType with + match getReflectionBaseType com ent with | Some d -> let genMap = Seq.zip ent.GenericParameters generics diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 2f18e27862..5fb4154f42 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2847,7 +2847,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr // | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args | _ -> - let e = makeImportLib com Any "ExceptionBase" "Types" + let e = makeImportLib com Any "ExceptionBase" "exception_bases" Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "get_Message", Some e -> Helper.GlobalCall("str", t, [ thisArg.Value ], ?loc = r) |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some @@ -4329,7 +4329,7 @@ let tryCall (com: ICompiler) (ctx: Context) r t (info: CallInfo) (thisArg: Expr let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genArgs args = match ent.FullName with - | Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "Types", args) + | Types.exception_ -> Some(makeImportLib com Any "ExceptionBase" "exception_bases", args) | Types.attribute -> Some(makeImportLib com Any "Attribute" "Types", args) | Types.dictionary -> let args = diff --git a/src/fable-library-py/fable_library/Fable.Library.Python.fsproj b/src/fable-library-py/fable_library/Fable.Library.Python.fsproj index 55b52d724b..b080b104a0 100644 --- a/src/fable-library-py/fable_library/Fable.Library.Python.fsproj +++ b/src/fable-library-py/fable_library/Fable.Library.Python.fsproj @@ -24,7 +24,7 @@ - + diff --git a/src/fable-library-py/fable_library/Native.fs b/src/fable-library-py/fable_library/Native.fs index 7b67f2e35c..1a9ac42bbc 100644 --- a/src/fable-library-py/fable_library/Native.fs +++ b/src/fable-library-py/fable_library/Native.fs @@ -6,6 +6,38 @@ module Native open Fable.Core open Fable.Core.PyInterop +[] +type ValueErrorBase(message: string, innerException: exn | null) = + inherit System.Exception(message, innerException) + +[] +type IndexErrorBase(message: string) = + inherit System.Exception(message) + +[] +type RuntimeErrorBase(message: string) = + inherit System.Exception(message) + +[] +type ZeroDivisionErrorBase(message: string) = + inherit System.Exception(message) + +[] +type OverflowErrorBase(message: string) = + inherit System.Exception(message) + +[] +type NotImplementedErrorBase(message: string) = + inherit System.Exception(message) + +[] +type MemoryErrorBase(message: string) = + inherit System.Exception(message) + +[] +type TimeoutErrorBase(message: string) = + inherit System.Exception(message) + [] [] type Cons<'T>(arrayType: string) = diff --git a/src/fable-library-py/fable_library/System.fs b/src/fable-library-py/fable_library/System.fs new file mode 100644 index 0000000000..a85c921c46 --- /dev/null +++ b/src/fable-library-py/fable_library/System.fs @@ -0,0 +1,96 @@ +namespace System + +// decision: keep Python exception ancestry local so other targets retain their existing bases. + +type SystemException(message: string) = + inherit Exception(message) + new() = SystemException(SR.Arg_SystemException) + +type ApplicationException(message: string) = + inherit Exception(message) + new() = ApplicationException(SR.Arg_ApplicationException) + +type ArithmeticException(message: string) = + inherit Exception(message) + new() = ArithmeticException(SR.Arg_ArithmeticException) + +type DivideByZeroException(message: string) = + inherit Native.ZeroDivisionErrorBase(message) + new() = DivideByZeroException(SR.Arg_DivideByZero) + +type FormatException(message: string) = + inherit Native.ValueErrorBase(message, Unchecked.defaultof) + new() = FormatException(SR.Arg_FormatException) + +type IndexOutOfRangeException(message: string) = + inherit Native.IndexErrorBase(message) + new() = IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException) + +type InvalidOperationException(message: string) = + inherit Native.RuntimeErrorBase(message) + new() = InvalidOperationException(SR.Arg_InvalidOperationException) + +type NotFiniteNumberException(message: string) = + inherit Exception(message) + new() = NotFiniteNumberException(SR.Arg_NotFiniteNumberException) + +type NotImplementedException(message: string) = + inherit Native.NotImplementedErrorBase(message) + new() = NotImplementedException(SR.Arg_NotImplementedException) + +type NotSupportedException(message: string) = + inherit Exception(message) + new() = NotSupportedException(SR.Arg_NotSupportedException) + +type NullReferenceException(message: string) = + inherit Exception(message) + new() = NullReferenceException(SR.Arg_NullReferenceException) + +type OutOfMemoryException(message: string) = + inherit Native.MemoryErrorBase(message) + new() = OutOfMemoryException(SR.Arg_OutOfMemoryException) + +type OverflowException(message: string) = + inherit Native.OverflowErrorBase(message) + new() = OverflowException(SR.Arg_OverflowException) + +type RankException(message: string) = + inherit Exception(message) + new() = RankException(SR.Arg_RankException) + +type StackOverflowException(message: string) = + inherit Exception(message) + new() = StackOverflowException(SR.Arg_StackOverflowException) + +type TimeoutException(message: string) = + inherit Native.TimeoutErrorBase(message) + new() = TimeoutException(SR.Arg_TimeoutException) + +type ArgumentException(message: string, paramName: string, innerException: exn | null) = + inherit + Native.ValueErrorBase( + (if System.String.IsNullOrEmpty(paramName) then + message + else + message + SR.Arg_ParamName_Name + paramName + "')"), + innerException + ) + + // Use Unchecked.defaultof rather than a `null` literal for the absent inner + // exception: Fable erases nullable reference annotations, so a `null` literal + // would be emitted against a non-nullable parameter type and fail type checking. + new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) + new(message) = ArgumentException(message, "", Unchecked.defaultof) + new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) + new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + member _.ParamName = paramName + +type ArgumentNullException(paramName: string, message: string) = + inherit ArgumentException(message, paramName, Unchecked.defaultof) + new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) + new() = ArgumentNullException("") + +type ArgumentOutOfRangeException(paramName: string, message: string) = + inherit ArgumentException(message, paramName, Unchecked.defaultof) + new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) + new() = ArgumentOutOfRangeException("") diff --git a/src/fable-library-py/fable_library/exception_bases.py b/src/fable-library-py/fable_library/exception_bases.py new file mode 100644 index 0000000000..6e66ff5c20 --- /dev/null +++ b/src/fable-library-py/fable_library/exception_bases.py @@ -0,0 +1,52 @@ +"""Python exception bases for generated .NET exception classes. + +decision: retain ExceptionBase ancestry and payloads while adding one specialized Python exception base. +""" + + +class ExceptionBase(Exception): + """Base class for .NET ``System.Exception`` and its subclasses. + + Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance`` + keep working as before. Only the message is forwarded to the built-in + initializer, so ``str(exc)`` still returns the message even when an inner + exception is supplied (the built-in would otherwise stringify the whole + argument tuple). The inner exception is kept on a dedicated attribute so it + can be read back through ``System.Exception.InnerException``. + """ + + def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None: + super().__init__(message if message is not None else "") + self.inner_exception: Exception | None = inner_exception + + +class ValueErrorBase(ExceptionBase, ValueError): + """A value error with .NET message and inner-exception support.""" + + +class IndexErrorBase(ExceptionBase, IndexError): + """An index error with .NET message and inner-exception support.""" + + +class RuntimeErrorBase(ExceptionBase, RuntimeError): + """A runtime error with .NET message and inner-exception support.""" + + +class ZeroDivisionErrorBase(ExceptionBase, ZeroDivisionError): + """A division error with .NET message and inner-exception support.""" + + +class OverflowErrorBase(ExceptionBase, OverflowError): + """An overflow error with .NET message and inner-exception support.""" + + +class NotImplementedErrorBase(ExceptionBase, NotImplementedError): + """An unimplemented operation with .NET message and inner-exception support.""" + + +class MemoryErrorBase(ExceptionBase, MemoryError): + """A memory error with .NET message and inner-exception support.""" + + +class TimeoutErrorBase(ExceptionBase, TimeoutError): + """A timeout error with .NET message and inner-exception support.""" diff --git a/src/fable-library-py/fable_library/types.py b/src/fable-library-py/fable_library/types.py index 1d140768dd..33af68d883 100644 --- a/src/fable-library-py/fable_library/types.py +++ b/src/fable-library-py/fable_library/types.py @@ -25,6 +25,7 @@ uint32, uint64, ) +from .exception_bases import ExceptionBase as ExceptionBase # Unit type for F# unit-typed parameters. Using None mirrors F#'s @@ -44,22 +45,6 @@ class Attribute: ... -class ExceptionBase(Exception): - """Base class for .NET ``System.Exception`` and its subclasses. - - Subclasses the built-in ``Exception`` so ``raise``/``except``/``isinstance`` - keep working as before. Only the message is forwarded to the built-in - initializer, so ``str(exc)`` still returns the message even when an inner - exception is supplied (the built-in would otherwise stringify the whole - argument tuple). The inner exception is kept on a dedicated attribute so it - can be read back through ``System.Exception.InnerException``. - """ - - def __init__(self, message: str | None = None, inner_exception: Exception | None = None) -> None: - super().__init__(message if message is not None else "") - self.inner_exception: Exception | None = inner_exception - - # We don't use type aliases here because we need to do isinstance checks. # # Generated code represents Int32 as a plain `int` and Float64 as a plain `float`, so diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 973873b61e..77534d182e 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1122,6 +1122,94 @@ let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") isNull (box ex.InnerException) |> equal true +[] +let ``test TimeoutException preserves message and empty InnerException`` () = + let ex = TimeoutException("timeout message") + ex.Message |> equal "timeout message" + isNull (box ex.InnerException) |> equal true + let caught = + try + raise ex + false + with + | :? TimeoutException as caught -> obj.ReferenceEquals(ex, caught) + caught |> equal true + +[] +let ``test exceptions sharing a Python base retain distinct dotnet types`` () = + let caught = + try + raise (FormatException("bad format")) + "not caught" + with + | :? ArgumentException -> "argument" + | :? FormatException as ex -> ex.Message + caught |> equal "bad format" + +#if FABLE_COMPILER +module private PythonExceptionBases = + open Fable.Core.PyInterop + + let valueError: obj = import "ValueError" "builtins" + let indexError: obj = import "IndexError" "builtins" + let runtimeError: obj = import "RuntimeError" "builtins" + let zeroDivisionError: obj = import "ZeroDivisionError" "builtins" + let overflowError: obj = import "OverflowError" "builtins" + let notImplementedError: obj = import "NotImplementedError" "builtins" + let memoryError: obj = import "MemoryError" "builtins" + let timeoutError: obj = import "TimeoutError" "builtins" + let exceptionBase: obj = import "ExceptionBase" "fable_library.exception_bases" + let legacyExceptionBase: obj = import "ExceptionBase" "fable_library.types" + let timeoutReflection: Func = import "TimeoutException_reflection" "fable_library.system" + let argumentNullReflection: Func = import "ArgumentNullException_reflection" "fable_library.system" + + [] + type TimeoutError() = inherit Exception() + +[] +let ``test Python ExceptionBase compatibility import preserves class identity`` () = + obj.ReferenceEquals(PythonExceptionBases.exceptionBase, PythonExceptionBases.legacyExceptionBase) |> equal true + +[] +let ``test dotnet exceptions also match their Python exception bases`` () = + let mappings: (exn * obj) list = [ + ArgumentException("message"), PythonExceptionBases.valueError + ArgumentNullException("parameter", "message"), PythonExceptionBases.valueError + ArgumentOutOfRangeException("parameter", "message"), PythonExceptionBases.valueError + FormatException("message"), PythonExceptionBases.valueError + IndexOutOfRangeException("message"), PythonExceptionBases.indexError + InvalidOperationException("message"), PythonExceptionBases.runtimeError + DivideByZeroException("message"), PythonExceptionBases.zeroDivisionError + OverflowException("message"), PythonExceptionBases.overflowError + NotImplementedException("message"), PythonExceptionBases.notImplementedError + OutOfMemoryException("message"), PythonExceptionBases.memoryError + TimeoutException("message"), PythonExceptionBases.timeoutError + ] + for ex, pythonBase in mappings do + Fable.Core.PyInterop.pyInstanceof ex pythonBase |> equal true + Fable.Core.PyInterop.pyInstanceof ex PythonExceptionBases.exceptionBase |> equal true + ex.Message.Contains("message") |> equal true + +[] +let ``test Python TimeoutError handler catches dotnet TimeoutException`` () = + let ex = TimeoutException("timeout message") + let caught = + try + raise ex + false + with + | :? PythonExceptionBases.TimeoutError as caught -> obj.ReferenceEquals(ex, caught) + caught |> equal true + +[] +let ``test Python exception reflection preserves dotnet ancestry`` () = + let timeoutType = PythonExceptionBases.timeoutReflection.Invoke() + timeoutType.IsSubclassOf(typeof) |> equal true + let argumentNullType = PythonExceptionBases.argumentNullReflection.Invoke() + argumentNullType.IsSubclassOf(typeof) |> equal true + argumentNullType.IsSubclassOf(typeof) |> equal true +#endif + [] let ``test use doesn't return on finally clause`` () = // See #211 let foo() = From 2da7144c68efded840fc6585c23d3a4e28e8e37f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 4 Oct 2026 14:45:00 +0200 Subject: [PATCH 132/322] fix(python): share per-object locks and align StartChild timeouts (#5035) --- src/fable-library-py/fable_library/async_.py | 35 +++-- src/fable-library-py/fable_library/util.py | 20 ++- tests/Python/TestAsync.fs | 75 +++++++++- tests/Python/TestMisc.fs | 108 ++++++++++++++ tests/Python/py/lock_runtime.py | 141 +++++++++++++++++++ 5 files changed, 354 insertions(+), 25 deletions(-) create mode 100644 tests/Python/py/lock_runtime.py diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 1b72efd6ee..7a5f46a02d 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -32,6 +32,7 @@ ) from .event import IEvent_2 from .protocols import IEnumerable_1 +from .system import TimeoutException__ctor from .task import TaskCompletionSource from .time_span import TimeSpan, to_milliseconds from .types import UNIT, Unit @@ -291,23 +292,29 @@ def cancel(_: OperationCanceledError) -> None: return tcs.get_task() -def start_child[T](computation: Async[T], ms: int | TimeSpan | None = None) -> Async[Async[T]]: - if ms is not None: - # Race the computation against a timeout: whichever settles first wins. - # asyncio.gather (the previous implementation via parallel2) waited for BOTH to settle, - # which meant the timeout always fired even when the computation finished first. - task = start_as_task(computation) +def _start_child_timeout[T](computation: Async[T], ms: int | TimeSpan) -> Async[Async[T]]: + # Return the child result when ready, or raise TimeoutException when the deadline expires. + task = asyncio.ensure_future(start_as_task(computation)) - async def with_timeout() -> T: - try: - return await asyncio.wait_for(task, timeout=to_milliseconds(ms) / 1000.0) - except TimeoutError: - raise TimeoutError() + async def with_timeout() -> T: + try: + return await asyncio.wait_for(task, timeout=to_milliseconds(ms) / 1000.0) + except TimeoutError: + # invariant: a completed child retains its exception; only deadline cancellation is translated + if not task.cancelled(): + raise + raise TimeoutException__ctor() - def cont(ctx: IAsyncContext[Async[T]]) -> None: - protected_return(await_task(with_timeout()))(ctx) + def cont(ctx: IAsyncContext[Async[T]]) -> None: + protected_return(await_task(with_timeout()))(ctx) - return protected_cont(cont) + return protected_cont(cont) + + +def start_child[T](computation: Async[T], ms: int | TimeSpan | None = None) -> Async[Async[T]]: + # decision: share TimeoutException across callers; its native base preserves Python TimeoutError handlers + if ms is not None: + return _start_child_timeout(computation, ms) task = start_as_task(computation) diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index c2abf502e5..1d3f5648ed 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -29,6 +29,7 @@ overload, ) from urllib.parse import quote, unquote +from weakref import WeakValueDictionary from .array_ import Array from .bases import ( @@ -351,16 +352,21 @@ def assert_not_equal[T](actual: T, expected: T, msg: str | None = None) -> None: raise Exception(msg or f"Expected not equal to: {expected} - Actual: {actual}") -MAX_LOCKS = 1024 +# decision: weak values give each active object its own lock without retaining idle locks or their objects +_locks: WeakValueDictionary[int, RLock] = WeakValueDictionary() +_lock_registry_guard = RLock() -def lock[T](lock_obj: Any, fn: Callable[[], T]) -> T: - @functools.lru_cache(maxsize=MAX_LOCKS) - def get_lock(n: int) -> RLock: - return RLock() +def lock[T](lock_obj: object, fn: Callable[[], T]) -> T: + key = id(lock_obj) + with _lock_registry_guard: + object_lock = _locks.get(key) + if object_lock is None: + object_lock = RLock() + _locks[key] = object_lock - lock = get_lock(id(lock_obj)) - with lock: + # invariant: each holder and waiter keeps a strong lock reference outside the registry guard + with object_lock: return fn() diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index 6db0fdf8c3..779d7f8653 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -467,7 +467,7 @@ let ``test Async.StartChild applies timeout`` () = let mutable x = "" let task = async { - x <- x + "A" + // A busy .NET runner can time out before this child is scheduled. do! Async.Sleep 1_000 x <- x + "X" // Never hit } @@ -482,8 +482,8 @@ let ``test Async.StartChild applies timeout`` () = x <- x + "C" - equal x "ABC" - } |> Async.StartImmediate + equal "BC" x + } |> Async.RunSynchronously [] let ``test Async.StartChild with timeout completes when computation finishes before timeout`` () = // See #4481 @@ -496,7 +496,74 @@ let ``test Async.StartChild with timeout completes when computation finishes bef with | :? TimeoutException -> failwith "should not time out" - } |> Async.StartImmediate + } |> Async.RunSynchronously + +let private assertChildFailure startChild (error: exn) = + async { + let failing: Async = async { + do! Async.Sleep 10 + return raise error + } + let! child = startChild(failing, 60_000) + let! result = Async.Catch child + match result with + | Choice2Of2 caught -> obj.ReferenceEquals(error, caught) |> equal true + | _ -> failwith "Expected child failure" + } |> Async.RunSynchronously + +[] +let ``test Async.StartChild preserves child TimeoutException`` () = + assertChildFailure + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (TimeoutException("child timeout")) + +let private assertChildTimeout startChild assertError = + async { + let pending: Async = Async.FromContinuations(ignore) + let! child = startChild(pending, 0) + let! result = Async.Catch child + match result with + | Choice2Of2 ex -> assertError ex + | _ -> failwith "Expected child timeout" + } |> Async.RunSynchronously + +[] +let ``test Async.StartChild zero timeout raises TimeoutException`` () = + assertChildTimeout + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (fun ex -> ex :? TimeoutException |> equal true) + +#if FABLE_COMPILER +module private NativeTimeout = + open Fable.Core + open Fable.Core.PyInterop + + let startChild: Func, int, Async>> = import "start_child" "fable_library.async_" + let timeoutError: obj = import "TimeoutError" "builtins" + + [] + let createTimeoutError (message: string) : exn = nativeOnly + +[] +let ``test Python start_child deadline matches both timeout types`` () = + assertChildTimeout + (fun (computation, timeout) -> NativeTimeout.startChild.Invoke(computation, timeout)) + (fun ex -> + ex :? TimeoutException |> equal true + Fable.Core.PyInterop.pyInstanceof ex NativeTimeout.timeoutError |> equal true) + +[] +let ``test Async.StartChild preserves child Python TimeoutError`` () = + assertChildFailure + (fun (computation, timeout) -> Async.StartChild(computation, timeout)) + (NativeTimeout.createTimeoutError "child timeout") + +[] +let ``test Python start_child preserves child TimeoutError`` () = + assertChildFailure + (fun (computation, timeout) -> NativeTimeout.startChild.Invoke(computation, timeout)) + (NativeTimeout.createTimeoutError "child timeout") +#endif [] let ``test Unit arguments are erased`` () = // See #1832 diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 77534d182e..44eca8a4cc 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1677,3 +1677,111 @@ let ``test exception variable captured in deferred single-arg closure`` () = with ex -> (fun (_: int) -> ex.Message) // lambda-with-arg captures ex sub 42 |> equal "boom" + +[] +let ``test lock is reentrant and returns the body value`` () = + let key = obj() + lock key (fun () -> lock key (fun () -> 42)) |> equal 42 + +[] +let ``test lock releases after a body exception`` () = + let key = obj() + let mutable error = "" + try + lock key (fun () -> failwith "body") + with ex -> + error <- ex.Message + equal "body" error + lock key (fun () -> 42) |> equal 42 + +#if FABLE_COMPILER +module private LockRuntime = + open Fable.Core.PyInterop + + type Threads = + abstract trace: string array + abstract errors: string array + abstract first_entered: bool + abstract second_attempted: bool + abstract second_blocked: bool + abstract threads_finished: bool + abstract record: string -> unit + abstract wait_release: unit -> bool + abstract run: Action * Action -> unit + + type Storage = + abstract stable: bool + abstract count: int + + let makeThreads: Func = import "make_threads" "./py/lock_runtime.py" + let makeStorage: Func = import "make_storage" "./py/lock_runtime.py" + let makeKey: Func = import "make_key" "./py/lock_runtime.py" + let makeCollidingKeys: Func = import "make_colliding_keys" "./py/lock_runtime.py" + +[] +let ``test real threads share a lock and release after an exception`` () = + let key = LockRuntime.makeKey.Invoke() + let threads = LockRuntime.makeThreads.Invoke() + let mutable error = "" + let first = Action(fun () -> + try + lock key (fun () -> + threads.record "first-enter" + threads.wait_release() |> equal true + threads.record "first-exit" + failwith "body") + with ex -> + error <- ex.Message) + let second = Action(fun () -> + lock key (fun () -> threads.record "second-enter") + threads.record "second-exit") + threads.run(first, second) + equal true threads.first_entered + equal true threads.second_attempted + equal true threads.second_blocked + equal true threads.threads_finished + equal [||] threads.errors + equal [| "first-enter"; "first-exit"; "second-enter"; "second-exit" |] threads.trace + equal "body" error + lock key (fun () -> 42) |> equal 42 + +[] +let ``test independent objects do not share a lock`` () = + let keys = LockRuntime.makeCollidingKeys.Invoke() + let threads = LockRuntime.makeThreads.Invoke() + let first = Action(fun () -> + lock keys.[0] (fun () -> + threads.record "first-enter" + threads.wait_release() |> equal true + threads.record "first-exit")) + let second = Action(fun () -> + lock keys.[1] (fun () -> threads.record "second-enter") + threads.record "second-exit") + threads.run(first, second) + equal true threads.first_entered + equal true threads.second_attempted + equal false threads.second_blocked + equal true threads.threads_finished + equal [||] threads.errors + equal 4 threads.trace.Length + +[] +let ``test lock storage releases idle entries across new objects`` () = + let iterations = 2048 + let storage = LockRuntime.makeStorage.Invoke() + let before = storage.count + lock (LockRuntime.makeKey.Invoke()) (fun () -> equal (before + 1) storage.count) + for _ in 1 .. iterations do + lock (LockRuntime.makeKey.Invoke()) (fun () -> 1) |> equal 1 + equal true storage.stable + equal before storage.count + +[] +let ``test lock storage releases idle entries after exceptions`` () = + let storage = LockRuntime.makeStorage.Invoke() + let before = storage.count + try + lock (LockRuntime.makeKey.Invoke()) (fun () -> failwith "body") + with _ -> () + equal before storage.count +#endif diff --git a/tests/Python/py/lock_runtime.py b/tests/Python/py/lock_runtime.py new file mode 100644 index 0000000000..22f29ffd71 --- /dev/null +++ b/tests/Python/py/lock_runtime.py @@ -0,0 +1,141 @@ +"""Thread and acquisition controls for the generated F# lock tests.""" + +import gc +from collections.abc import Callable +from threading import Event, RLock, Thread +from types import TracebackType +from unittest.mock import patch +from weakref import WeakValueDictionary + +from fable_library import util +from fable_library.array_ import Array + + +LEGACY_STRIPE_COUNT = 1024 + + +class Acquisition: + def __init__(self) -> None: + self.attempted = Event() + self.blocked = False + + +class ObservedLock: + def __init__(self, acquisition: Acquisition) -> None: + self.lock = RLock() + self.acquisition = acquisition + + def __enter__(self) -> "ObservedLock": + acquired = self.lock.acquire(blocking=False) + self.acquisition.blocked = not acquired + # invariant: the contender's observation follows an actual acquisition attempt + self.acquisition.attempted.set() + if not acquired: + self.lock.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.lock.release() + + +class Threads: + def __init__(self) -> None: + self.entered = Event() + self.release = Event() + self._trace: list[str] = [] + self._errors: list[str] = [] + self.first_entered = False + self.second_attempted = False + self.second_blocked = False + self.threads_finished = False + + @property + def trace(self) -> Array[str]: + return Array(self._trace) + + @property + def errors(self) -> Array[str]: + return Array(self._errors) + + def record(self, entry: str) -> None: + self._trace.append(entry) + if entry == "first-enter": + self.entered.set() + + def wait_release(self) -> bool: + return self.release.wait(5) + + def run(self, first: Callable[[], None], second: Callable[[], None]) -> None: + acquisition = Acquisition() + + def invoke(action: Callable[[], None]) -> None: + try: + action() + except Exception as error: + self._errors.append(str(error)) + + first_thread = Thread(target=invoke, args=(first,), daemon=True) + second_thread = Thread(target=invoke, args=(second,), daemon=True) + with ( + patch.object(util, "RLock", lambda: ObservedLock(acquisition)), + patch.object(util, "_locks", WeakValueDictionary()), + ): + first_thread.start() + try: + self.first_entered = self.entered.wait(5) + if self.first_entered: + acquisition.attempted.clear() + second_thread.start() + self.second_attempted = acquisition.attempted.wait(5) + self.second_blocked = acquisition.blocked + finally: + self.release.set() + first_thread.join(5) + if second_thread.ident is not None: + second_thread.join(5) + self.threads_finished = not first_thread.is_alive() and not second_thread.is_alive() + + +class Storage: + def __init__(self) -> None: + self.before = getattr(util, "_locks", ()) + + @property + def stable(self) -> bool: + return getattr(util, "_locks", None) is self.before + + @property + def count(self) -> int: + gc.collect() + return len(getattr(util, "_locks", ())) + + +def make_key() -> object: + # decision: uses an unhashable, non-weak-referenceable key to cover arbitrary lock objects + return [] + + +def make_colliding_keys() -> Array[object]: + # decision: reproduces the former stripe collision using distinct, unhashable objects + keys: dict[int, object] = {} + for _ in range(LEGACY_STRIPE_COUNT + 1): + key: object = [] + stripe = (id(key) >> 4) % LEGACY_STRIPE_COUNT + previous = keys.get(stripe) + if previous is not None: + return Array([previous, key]) + keys[stripe] = key + raise AssertionError("Expected a collision in the former fixed stripes") + + +def make_threads() -> Threads: + return Threads() + + +def make_storage() -> Storage: + return Storage() From b9ce2fdb53fc86a615970f4828005ee3a490385f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 4 Oct 2026 21:42:40 +0200 Subject: [PATCH 133/322] fix(beam): prevent identifier collisions and atom overflow (#5017) Co-authored-by: Maxime Mangel --- src/Fable.Build/Test/Beam.fs | 73 ++++ src/Fable.Transforms/Beam/FABLE-BEAM.md | 15 +- .../Beam/Fable2Beam.Reflection.fs | 2 +- src/Fable.Transforms/Beam/Fable2Beam.Util.fs | 2 +- src/Fable.Transforms/Beam/Fable2Beam.fs | 376 +++++++++++++++--- src/Fable.Transforms/Beam/Prelude.fs | 74 +++- src/Fable.Transforms/Beam/Replacements.fs | 7 +- src/Fable.Transforms/Global/Naming.fs | 10 + src/Fable.Transforms/OverloadSuffix.fs | 12 +- src/fable-standalone/src/Main.fs | 2 + src/fable-standalone/test/worker/run.mjs | 25 ++ .../CollidingMemberNames.fsproj | 13 + tests/Beam/CollidingMemberNames/Program.fs | 51 +++ tests/Beam/MiscTests.fs | 31 +- tests/Beam/ModuleNamingTests.fs | 104 +++++ tests/Beam/RecordTests.fs | 14 +- .../Beam/UnsupportedNumericLiteral/Program.fs | 6 + 17 files changed, 733 insertions(+), 84 deletions(-) create mode 100644 tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj create mode 100644 tests/Beam/CollidingMemberNames/Program.fs diff --git a/src/Fable.Build/Test/Beam.fs b/src/Fable.Build/Test/Beam.fs index 58e82044a4..bb2126743a 100644 --- a/src/Fable.Build/Test/Beam.fs +++ b/src/Fable.Build/Test/Beam.fs @@ -121,6 +121,16 @@ let private testUnsupportedDiagnostics () = "Program.fs(3,12): (3,31) error FABLE: Numeric literal kind 'Float16' is not supported for Beam target" output + expectContains + "colliding record fields have an actionable diagnostic" + "Record 'Fable.Tests.UnsupportedNumericLiteral.CollidingRecord' has fields 'foo-bar' and 'foo_bar' that both compile to Erlang atom 'foo_bar_'. Rename one of the fields." + output + + expectContains + "colliding anonymous record fields have an actionable diagnostic" + "Anonymous record has fields 'foo-bar' and 'foo_bar' that both compile to Erlang atom 'foo_bar_'. Rename one of the fields." + output + let generatedFile = Path.Combine(programBuildDir, "src", "unsupported_numeric_literal_program.erl") @@ -131,6 +141,68 @@ let private testUnsupportedDiagnostics () = false (generatedCode.Contains("erlang:error(unsupported_")) +let private testIdentifierCollisionDiagnostics () = + let programSourceDir = Path.Resolve("tests", "Beam", "CollidingMemberNames") + let programBuildDir = Path.Resolve("temp", "tests", "BeamCollidingMemberNames") + Directory.clean programBuildDir + + let output, exitCode = + runFable + programBuildDir + [ + programSourceDir + "--outDir" + programBuildDir + "--lang" + "beam" + "--exclude" + "Fable.Core" + "--noCache" + ] + + expect "colliding member name compilation fails" 1 exitCode + + expectContains + "colliding member names have an actionable diagnostic" + "Declarations 'fooBar' and 'foo_bar' in 'colliding_member_names_program' compile to duplicate Erlang function 'foo_bar/0'. Rename one declaration." + output + + expectContains + "class-generated and module member collisions have an actionable diagnostic" + "Declarations in 'colliding_member_names_program' compile to duplicate Erlang function 'foo_ctor/0'. Rename one declaration or change its arity." + output + + expectContains + "members differing only in case have an actionable diagnostic" + "compile to duplicate Erlang function 'renderer_work/1'. Rename one declaration or change its arity." + output + + for owner in [ "CtorFields"; "ValFields" ] do + expectContains + $"colliding fields on '%s{owner}' have an actionable diagnostic" + $"Type 'Fable.Tests.CollidingMemberNames.%s{owner}' has fields 'fooBar' and 'FooBar' that both compile to Erlang atom 'field_foo_bar'. Rename one of the fields." + output + + expectContains + "colliding union case tags have an actionable diagnostic" + "Union 'Fable.Tests.CollidingMemberNames.CollidingUnion' has cases 'FooBar' and 'Foo_Bar' that both compile to Erlang atom 'foo_bar'. Rename one of the cases." + output + + expectContains + "a CompiledName colliding with a case name has an actionable diagnostic" + "Union 'Fable.Tests.CollidingMemberNames.CompiledNameUnion' has cases 'Tagged' and 'Foo_Bar' that both compile to Erlang atom 'foo_bar'. Rename one of the cases." + output + + expectContains + "colliding interface members have an actionable diagnostic" + "Interface 'Fable.Tests.CollidingMemberNames.ICollidingMembers' has members 'FooBar' and 'Foo_Bar' that both dispatch through Erlang atom 'foo_bar'. Rename one of the members." + output + + expectContains + "bounded interface setter keys participate in collision diagnostics" + "Interface 'Fable.Tests.CollidingMemberNames.ILongSetterCollision' has members" + output + /// Compile a whole program and run it on the BEAM through the generated `main.erl` shim. /// /// The test suite calls test functions directly and so never executes the shim — which is how the @@ -296,3 +368,4 @@ let handle (args: string list) = // Unsupported constructs must stop compilation at their source location rather than leave // an erlang:error placeholder that fails only when the generated code runs. testUnsupportedDiagnostics () + testIdentifierCollisionDiagnostics () diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index eb0e074d2b..7602a28c66 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -113,8 +113,16 @@ Exceptions: - native Erlang modules referenced through interop keep their native names. Function, field, case, and module names are sanitized to Erlang atoms. Reserved -words receive a trailing underscore. Reflection metadata stores both the original -F# name and the emitted atom when both are needed. +words receive a trailing underscore. Encoded punctuation in F# compiled names uses +`_xNNNN_` segments so distinct backticked identifiers do not collapse to one atom; +remaining module-level name/arity collisions fail compilation. Reflection metadata +stores both the original F# name and the emitted atom when both are needed. + +Names are capped at 223 characters to leave room for atoms generated by `erlc`. +Native compilation uses a SHA-256 suffix for overlong names. Standalone compilation +uses the existing 32-bit djb2 hash to avoid cryptography dependencies in the web UI; +overlong names can therefore differ between compiler hosts. Detected collisions, +including module-value state keys, fail compilation. The generated project contains: @@ -478,7 +486,7 @@ suite grows. | Options | Erasure can still conflate `None`, `Some null`/`Some undefined`, and some nested option paths after static type information is lost. | | `char` | A generic or `obj`-erased character is an integer at runtime, so `string` and `%A` can print its codepoint. UTF-16 surrogate behavior is not complete. | | Structured formatting | `%A` reconstructs values from term shape. Record field order, original names, erased options, sets, chars, refs/arrays, decimals, and date/time values can differ from .NET output. | -| Identifiers | Record fields containing spaces or symbols are not fully supported. Sanitized names also need collision diagnostics where distinct F# names produce the same Erlang atom. | +| Identifiers | Instance members split across declaration paths can still sanitize to the same Erlang function name and arity. | | Type tests | Erlang cannot distinguish integer widths or unrelated F# types with the same runtime shape. Some interface/class downcasts and abstract/base dispatch paths are unsupported. | | Classes and structs | Mutable record fields, class reference equality, some self-referencing/base constructors, mutually recursive class hierarchies, and default struct construction remain incomplete. | | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | @@ -516,7 +524,6 @@ visible rather than silent correctness failures. | Priority | Gap | Suggested direction | | --- | --- | --- | -| P0 | Unsupported AST paths can survive compilation | Do not silently emit runtime placeholders for constructs inside the supported surface. Prefer compiler errors with source ranges for statically detectable unsupported constructs. | | P0 | Union declaration ordering is incomplete | Thread union-aware comparers through nested comparison and ordered collections. If type-directed routing cannot cover generic containers, define a versioned DU/collection representation change. | | P0 | Option erasure loses states in generic and null-like paths | Carry the nested-option decision through replacements and collection helpers, or adopt an unambiguous tagged form where erasure is unsafe. | | P0 | Module initialization is process-dependent | Define library initialization semantics. Prefer explicit generated initialization invoked by entry points/process owners; use global storage only if cross-process mutation is intentionally supported. | diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs b/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs index c37527fe1a..6e0cf00447 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Reflection.fs @@ -43,7 +43,7 @@ let private makeThunk (elements: Beam.ErlExpr list) = /// Name of the per-entity reflection function generated for a record/union /// (e.g. entity `Tree` -> `tree_reflection/0`). let reflectionFuncName (declarationName: string) = - Fable.Beam.Naming.sanitizeErlangName declarationName + "_reflection" + Fable.Beam.Naming.limitErlangAtomLength (Fable.Beam.Naming.sanitizeErlangName declarationName + "_reflection") /// Erlang variable names bound to an entity's resolved generic arguments inside its /// reflection function (`gen0`, `gen1`, ...). diff --git a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs index 9b620dc4a0..edafa0d4de 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.Util.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.Util.fs @@ -588,4 +588,4 @@ let isolateScope (body: Beam.ErlExpr list) : Beam.ErlExpr = /// and the declaration/initializer must all derive their key through this helper or state /// silently breaks. let mutableStateKey (moduleName: string) (name: string) = - moduleName + "_" + Naming.sanitizeErlangName name + Naming.limitErlangAtomLength (moduleName + "_" + Naming.sanitizeErlangName name) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 30d9511a16..87d48dd16f 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -85,6 +85,7 @@ type Context = DecisionTargets: (Ident list * Expr) list RecursiveBindings: Set MutualRecBindings: Map // name -> (bundleVarName, atomTag) + PrivateClassBindings: Map // Fable compiled name -> disambiguated Erlang function name MutableVars: Map // F# ident name -> Erlang var name holding the ref LocalVars: Set // names of locally-bound variables (params, let bindings, lambda args) ThisArgVar: string option // Erlang variable name for `this` in class constructors/methods @@ -134,12 +135,86 @@ let private hasMutableRecordFields (com: IBeamCompiler) (typ: Fable.AST.Fable.Ty let private atomLit name = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom name)) +let private functionName (ctx: Context) name = + ctx.PrivateClassBindings + |> Map.tryFind name + |> Option.defaultWith (fun () -> sanitizeErlangName name) + let private unsupportedExpression (com: IBeamCompiler) range message = com.AddLog(message, Severity.Error, ?range = range, fileName = com.CurrentFile, tag = "FABLE") // invariant: Unsupported source constructs log an error before returning a recovery expression. atomLit "undefined" +let private reportFunctionNameCollision (com: IBeamCompiler) owner (Beam.Atom name) arity = + // invariant: Distinct declarations have distinct Erlang function names and arities — Erlang resolves functions only by that pair. + com.AddLog( + $"Declarations in '%s{owner}' compile to duplicate Erlang function '%s{name}/%d{arity}'. Rename one declaration or change its arity.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + +let private reportFieldNameCollisionsBy (com: IBeamCompiler) owner toAtom (fieldNames: string seq) = + fieldNames + |> Seq.groupBy toAtom + |> Seq.iter (fun (erlName, fields) -> + let fields = fields |> Seq.distinct |> Seq.toList + + if fields.Length > 1 then + let names = fields |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Distinct record fields map to distinct Erlang atoms — collisions stop compilation before a map value is lost. + com.AddLog( + $"%s{owner} has fields %s{names} that both compile to Erlang atom '%s{erlName}'. Rename one of the fields.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + +let private reportFieldNameCollisions (com: IBeamCompiler) owner (fieldNames: string seq) = + reportFieldNameCollisionsBy com owner sanitizeFieldName fieldNames + +let private reportUnionCaseTagCollisions (com: IBeamCompiler) (ent: Entity) = + ent.UnionCases + |> List.groupBy (fun case -> unionCaseTagName case.CompiledName case.Name) + |> List.iter (fun (erlTag, cases) -> + let names = cases |> List.map (fun case -> case.Name) |> List.distinct + + if names.Length > 1 then + let names = names |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Distinct union cases carry distinct tags — a shared tag makes `match` pick + // the wrong branch and two different values compare equal, with nothing to see at runtime. + com.AddLog( + $"Union '%s{ent.FullName}' has cases %s{names} that both compile to Erlang atom '%s{erlTag}'. Rename one of the cases.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + +let private reportInterfaceMemberCollisions (com: IBeamCompiler) (ent: Entity) = + ent.MembersFunctionsAndValues + |> Seq.groupBy (fun memb -> interfaceMemberKey memb.IsSetter memb.DisplayName) + |> Seq.iter (fun (erlName, members) -> + let names = + members |> Seq.map (fun memb -> memb.DisplayName) |> Seq.distinct |> Seq.toList + + if names.Length > 1 then + let names = names |> List.map (sprintf "'%s'") |> String.concat " and " + + // invariant: Interface members are keys in the implementation map, so a shared key makes + // one member silently resolve to the other's body. + com.AddLog( + $"Interface '%s{ent.FullName}' has members %s{names} that both dispatch through Erlang atom '%s{erlName}'. Rename one of the members.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + /// Resolve the atom tag name for a union case. /// Returns Some(atomStr, isFieldless) or None if the entity can't be resolved. let private getUnionCaseAtomExpr (com: IBeamCompiler) (ref: EntityRef) (tag: int) = @@ -275,7 +350,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er Beam.ErlExpr.Call(None, "get", [ atomLit (mutableStateKey ctx.ModuleName ident.Name) ]) else // Module-level function reference: call as 0-arity function - Beam.ErlExpr.Call(None, sanitizeErlangName ident.Name, []) + Beam.ErlExpr.Call(None, functionName ctx ident.Name, []) | Sequential exprs -> let erlExprs = exprs |> List.map (transformExpr com ctx) @@ -615,10 +690,9 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er match expr.Type with | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> - let sanitizedFieldName = sanitizeErlangName fieldName // Use f$ prefix to avoid collision with interface method keys let atomField = - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom("field_" + sanitizedFieldName))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(classFieldAtomName fieldName))) // Class instance: update state in process dict // put(Ref, maps:put(field, Value, get(Ref))) Beam.ErlExpr.Call( @@ -636,7 +710,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> // Interface property setter: dispatch through set_ prefixed key // (fable_utils:iface_get(set_bar, Obj))(Value) - let setterKey = "set_" + sanitizeErlangName fieldName + let setterKey = interfaceMemberKey true fieldName let setterAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom setterKey)) let lookup = @@ -1237,7 +1311,7 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er } ] - atomLit ("set_" + methodName), funExpr + atomLit (interfaceMemberKey true memb.Name), funExpr else let argPats = nonThisArgs |> List.map (fun a -> Beam.PVar(toErlangVar a)) @@ -1639,7 +1713,7 @@ and transformValue |> List.map (fun (name, value) -> let fieldKey = if ctx.ClassFieldPrefix then - atomLit ("field_" + sanitizeErlangName name) + atomLit (classFieldAtomName name) else Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(sanitizeFieldName name))) @@ -1680,6 +1754,8 @@ and transformValue Beam.ErlExpr.Map entries | NewAnonymousRecord(values, fieldNames, _genArgs, _isStruct) -> + reportFieldNameCollisions com "Anonymous record" fieldNames + let entries = Array.zip fieldNames (values |> List.toArray) |> Array.toList @@ -2128,40 +2204,40 @@ and transformGet (com: IBeamCompiler) (ctx: Context) (kind: GetKind) (typ: Type) let funcName = sanitizeErlangName info.Name Beam.ErlExpr.Call(moduleName, funcName, []) | _ -> + // During construction, the Fable AST can type `this` as Any inside a captured + // object expression. Resolve already-initialized fields from the constructor cache + // before dispatching on that unreliable static type. + let isThisRef = + match expr, ctx.ThisArgVar, erlExpr with + | Value(ThisValue _, _), _, _ -> true + | IdentExpr ident, _, _ when ident.Name.StartsWith("_this", System.StringComparison.Ordinal) -> true + | _, Some thisVar, Beam.ErlExpr.Variable v -> v = thisVar + | _ -> false - match expr.Type with - | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> - let fieldName = sanitizeErlangName info.Name - - // During constructor, field values may reference other fields via this.FieldName. - // Since put(Ref, #{...}) hasn't happened yet, we use the precomputed Erlang expressions. - let isThisRef = - match ctx.ThisArgVar, erlExpr with - | Some thisVar, Beam.ErlExpr.Variable v -> v = thisVar - | _ -> false - - match isThisRef, ctx.CtorFieldExprs.TryFind(info.Name) with - | true, Some cachedExpr -> cachedExpr - | _ -> + match isThisRef, ctx.CtorFieldExprs.TryFind(info.Name) with + | true, Some cachedExpr -> cachedExpr + | _ -> + match expr.Type with + | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isClassType com entityRef -> // Class instance: read via fable_utils:field_get, which supports both // a self-contained map (immutable class) and a process-dict ref (mutable class). // Use field_ prefix to avoid collision with interface method keys. let classFieldAtom = - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom("field_" + fieldName))) + Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom(classFieldAtomName info.Name))) Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ classFieldAtom; erlExpr ]) - | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> - let fieldName = sanitizeErlangName info.Name - let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) - // Interface dispatch: works for both object expressions (maps) and class instances (refs). - // Class interface property getters are stored as 0-arity thunks — iface_get calls them. - // ObjectExpr property getters are stored as plain values — iface_get returns them as-is. - Beam.ErlExpr.Call(Some "fable_utils", "iface_get", [ fieldAtom; erlExpr ]) - | _ -> - // Record/union/anonymous record: direct map access, use sanitizeFieldName for disambiguation - let fieldName = sanitizeFieldName info.Name - let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) - Beam.ErlExpr.Call(Some "maps", "get", [ fieldAtom; erlExpr ]) + | Fable.AST.Fable.Type.DeclaredType(entityRef, _) when isInterfaceType com entityRef -> + let fieldName = sanitizeErlangName info.Name + let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) + // Interface dispatch: works for both object expressions (maps) and class instances (refs). + // Class interface property getters are stored as 0-arity thunks — iface_get calls them. + // ObjectExpr property getters are stored as plain values — iface_get returns them as-is. + Beam.ErlExpr.Call(Some "fable_utils", "iface_get", [ fieldAtom; erlExpr ]) + | _ -> + // Record/union/anonymous record: direct map access, use sanitizeFieldName for disambiguation + let fieldName = sanitizeFieldName info.Name + let fieldAtom = Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom fieldName)) + Beam.ErlExpr.Call(Some "maps", "get", [ fieldAtom; erlExpr ]) | ExprGet indexExpr -> let erlIndex = transformExpr com ctx indexExpr @@ -2510,7 +2586,8 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call // Field-stored function: (fable_utils:field_get(field_, This))(Args) let erlThis = transformExpr com ctx thisExpr let thisH, cleanThis = extractBlock erlThis - let fieldAtom = atomLit ("field_" + sanitizeErlangName ident.Name) + + let fieldAtom = atomLit (classFieldAtomName ident.Name) let lookup = Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ fieldAtom; cleanThis ]) @@ -2562,7 +2639,7 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call apply |> wrapWithHoisted allHoisted else - Beam.ErlExpr.Call(None, sanitizeErlangName ident.Name, allArgs) + Beam.ErlExpr.Call(None, functionName ctx ident.Name, allArgs) |> wrapWithHoisted allHoisted | Get(calleeExpr, FieldGet fieldInfo, _, _) -> @@ -2660,7 +2737,7 @@ and transformCall (com: IBeamCompiler) (ctx: Context) (callee: Expr) (info: Call if isCtorFieldInvoke then // Constructor param field invoke: (fable_utils:field_get(field_, This))(Args) - let fieldAtom = atomLit ("field_" + sanitizeErlangName methodName) + let fieldAtom = atomLit (classFieldAtomName methodName) let lookup = Beam.ErlExpr.Call(Some "fable_utils", "field_get", [ fieldAtom; cleanCallee ]) @@ -2694,6 +2771,21 @@ and transformClassDeclaration (decl: ClassDecl) : Beam.ErlForm list = + // Records key their map by `sanitizeFieldName`; every other type keys it by `classFieldAtomName`, + // which collapses more names, so each has to be checked against the atom it actually emits. + if ent.IsFSharpRecord then + ent.FSharpFields + |> List.map (fun field -> field.Name) + |> reportFieldNameCollisions com $"Record '%s{ent.FullName}'" + elif ent.IsFSharpUnion then + reportUnionCaseTagCollisions com ent + elif ent.IsInterface then + reportInterfaceMemberCollisions com ent + else + ent.FSharpFields + |> List.map (fun field -> field.Name) + |> reportFieldNameCollisionsBy com $"Type '%s{ent.FullName}'" classFieldAtomName + let constructorForms = match decl.Constructor with | Some cons -> @@ -2934,7 +3026,7 @@ and transformClassDeclaration else transformExpr com fieldCtx value - let entries' = entries @ [ atomLit ("field_" + sanitizeErlangName name), erlValue ] + let entries' = entries @ [ atomLit (classFieldAtomName name), erlValue ] let fieldCtx' = { fieldCtx with CtorFieldExprs = fieldCtx.CtorFieldExprs.Add(name, erlValue) } @@ -3036,11 +3128,7 @@ and transformClassDeclaration ] // Setters use set_ prefix to avoid key collision with getters - let keyName = - if sigInfo.IsSetter then - "set_" + memberName - else - memberName + let keyName = interfaceMemberKey sigInfo.IsSetter memb.Name Some(atomLit keyName, closure) | Some _ @@ -3254,7 +3342,7 @@ and transformClassDeclaration elif not memb.IsMangled && info.IsGetter then // Property getter: class_name_get_prop(This) -> maps:get(prop, get(This)). let propName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_%s{propName}" + let funcName = limitErlangAtomLength $"%s{className}_%s{propName}" let _thisArg, _nonThisArgs, memberCtx = getThisAndArgs () let bodyExpr = transformExpr com memberCtx memb.Body @@ -3282,7 +3370,7 @@ and transformClassDeclaration elif not memb.IsMangled && info.IsSetter then // Property setter: class_name_set_prop(This, Value) -> put(This, maps:put(prop, Value, get(This))). let propName = sanitizeErlangName memb.Name - let funcName = $"%s{className}_set_%s{propName}" + let funcName = limitErlangAtomLength $"%s{className}_set_%s{propName}" let _thisArg, nonThisArgs, memberCtx = getThisAndArgs () let argPatterns = @@ -3407,6 +3495,15 @@ and transformClassDeclaration // syntax, Erlang uses plain functions so name collisions produce duplicate definitions. let allForms = constructorForms @ memberForms @ reflectionForms + let reflectionKeys = + reflectionForms + |> List.choose ( + function + | Beam.ErlForm.Function def -> Some(def.Name, def.Arity) + | _ -> None + ) + |> Set.ofList + let dedup = allForms |> List.fold @@ -3416,6 +3513,9 @@ and transformClassDeclaration let key = (def.Name, def.Arity) if Set.contains key seen then + if not (Set.contains key reflectionKeys) then + reportFunctionNameCollision com ent.FullName def.Name def.Arity + (seen, acc) else (Set.add key seen, form :: acc) @@ -3435,7 +3535,7 @@ and transformDeclaration (com: IBeamCompiler) (ctx: Context) (decl: Declaration) |> Option.map com.GetMember |> Option.defaultWith (fun () -> com.GetMember(memDecl.MemberRef)) - let name = sanitizeErlangName memDecl.Name + let name = functionName ctx memDecl.Name // Process-dict key for module-level mutable/snapshot values, namespaced by module. let stateKey = mutableStateKey ctx.ModuleName memDecl.Name @@ -3753,6 +3853,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = DecisionTargets = [] RecursiveBindings = Set.empty MutualRecBindings = Map.empty + PrivateClassBindings = Map.empty MutableVars = Map.empty LocalVars = Set.empty ThisArgVar = None @@ -3811,9 +3912,190 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) } + let rec collectMemberDeclarations declarations = + declarations + |> List.collect ( + function + | MemberDeclaration memDecl -> + let info = + memDecl.ImplementedSignatureRef + |> Option.map beamCom.GetMember + |> Option.defaultWith (fun () -> beamCom.GetMember(memDecl.MemberRef)) + + let arity = + match memDecl.Args with + | _thisArg :: rest when info.IsInstance -> + 1 + (rest |> FSharp2Fable.Util.discardUnitArg |> List.length) + | args -> args |> FSharp2Fable.Util.discardUnitArg |> List.length + + [ memDecl, info, (Beam.Atom(sanitizeErlangName memDecl.Name), arity) ] + | ModuleDeclaration modDecl -> collectMemberDeclarations modDecl.Members + | ActionDeclaration _ + | ClassDeclaration _ -> [] + ) + + collectMemberDeclarations file.Declarations + |> List.choose (fun (memDecl, info, (_, arity)) -> + if + info.IsValue + && not info.IsInstance + && arity = 0 + && (info.IsMutable || readsFreeMutable Set.empty memDecl.Body) + then + Some(mutableStateKey moduleName memDecl.Name, memDecl.Name) + else + None + ) + |> List.groupBy fst + |> List.iter (fun (stateKey, declarations) -> + let names = declarations |> List.map snd |> List.distinct + + if names.Length > 1 then + let names = + names + |> List.map (decodeFSharpCompiledName >> sprintf "'%s'") + |> String.concat " and " + + // invariant: Distinct module values cannot share a process-dictionary key, even when their function names remain distinct. + beamCom.AddLog( + $"Module values %s{names} in '%s{moduleName}' share Erlang state key '%s{stateKey}'. Rename one declaration.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + ) + + let privateClassBindings = + collectMemberDeclarations file.Declarations + |> List.groupBy (fun (_, _, key) -> key) + |> List.collect (fun (_, declarations) -> + declarations + |> List.choose (fun (memDecl, info, _) -> + let separatorIndex = memDecl.Name.LastIndexOf("__", System.StringComparison.Ordinal) + + let memberName = + if separatorIndex < 0 then + memDecl.Name + else + memDecl.Name.Substring(separatorIndex + 2) + + let hasUpperCasePeer = + declarations + |> List.exists (fun (otherDecl, _, _) -> + let otherSeparatorIndex = + otherDecl.Name.LastIndexOf("__", System.StringComparison.Ordinal) + + let otherMemberName = + if otherSeparatorIndex < 0 then + otherDecl.Name + else + otherDecl.Name.Substring(otherSeparatorIndex + 2) + + not (System.String.Equals(otherDecl.Name, memDecl.Name, System.StringComparison.Ordinal)) + && System.String.Equals( + otherDecl.Name, + memDecl.Name, + System.StringComparison.OrdinalIgnoreCase + ) + && not (System.String.IsNullOrEmpty otherMemberName) + && System.Char.IsUpper(otherMemberName[0]) + ) + + // invariant: A captured local has no enclosing entity to be a member of, so no other + // file can name it. `functionName` is consulted on the local-call paths only, so + // renaming anything reachable from elsewhere emits the old atom at that call site. + let isCapturedLocal = info.ApparentEnclosingEntity.IsNone + + if + info.IsInstance + && isCapturedLocal + && not (System.String.IsNullOrEmpty memberName) + && System.Char.IsLower(memberName[0]) + && hasUpperCasePeer + then + // decision: A captured `let work` keeps a separate generated function from + // `member Work`; F# distinguishes their case, while Erlang function atoms do not. + Some(memDecl.Name, sanitizeErlangName ("fable_private_" + memDecl.Name)) + else + None + ) + ) + |> Map.ofList + + let ctx = { ctx with PrivateClassBindings = privateClassBindings } + + let rec collectModuleMemberNames declarations = + declarations + |> List.collect ( + function + | MemberDeclaration memDecl -> + let info = + memDecl.ImplementedSignatureRef + |> Option.map beamCom.GetMember + |> Option.defaultWith (fun () -> beamCom.GetMember(memDecl.MemberRef)) + + if info.IsInstance then + [] + else + let arity = memDecl.Args |> FSharp2Fable.Util.discardUnitArg |> List.length + [ (Beam.Atom(sanitizeErlangName memDecl.Name), arity), memDecl.Name ] + | ModuleDeclaration modDecl -> collectModuleMemberNames modDecl.Members + | ActionDeclaration _ + | ClassDeclaration _ -> [] + ) + + let reportedModuleCollisions = + collectModuleMemberNames file.Declarations + |> List.groupBy fst + |> List.choose (fun ((name, arity), declarations) -> + let sourceNames = + declarations + |> List.map snd + |> List.distinct + |> List.map decodeFSharpCompiledName + + if sourceNames.Length > 1 then + let names = sourceNames |> List.map (sprintf "'%s'") |> String.concat " and " + let (Beam.Atom erlName) = name + + // invariant: Distinct module declarations map to distinct Erlang function keys — otherwise one body is silently dropped. + beamCom.AddLog( + $"Declarations %s{names} in '%s{moduleName}' compile to duplicate Erlang function '%s{erlName}/%d{arity}'. Rename one declaration.", + Severity.Error, + fileName = com.CurrentFile, + tag = "FABLE" + ) + + Some(name, arity) + else + None + ) + |> Set.ofList + + let declarationForms = + file.Declarations |> List.collect (transformDeclaration beamCom ctx) + + declarationForms + |> List.choose ( + function + | Beam.ErlForm.Function def -> Some((def.Name, def.Arity), def) + | _ -> None + ) + |> List.groupBy fst + |> List.iter (fun ((name, arity), definitions) -> + let key = name, arity + + if + definitions.Length > 1 + && key <> (Beam.Atom "main", 0) + && not (Set.contains key reportedModuleCollisions) + then + // Class constructors, members, and reflection helpers only become visible after transformation. + reportFunctionNameCollision beamCom moduleName name arity + ) + let forms = - file.Declarations - |> List.collect (transformDeclaration beamCom ctx) + declarationForms // Deduplicate functions by name+arity at module level. This handles cases where // a property setter and a method mangle to the same Erlang function name, even // when they come from different declaration paths (ClassDeclaration vs MemberDeclaration). diff --git a/src/Fable.Transforms/Beam/Prelude.fs b/src/Fable.Transforms/Beam/Prelude.fs index 4a17f395bb..994403ad9b 100644 --- a/src/Fable.Transforms/Beam/Prelude.fs +++ b/src/Fable.Transforms/Beam/Prelude.fs @@ -66,11 +66,55 @@ module Naming = sb.ToString() - /// Drop the characters an F# name may carry that an unquoted Erlang atom cannot + /// Decode F# `$XXXX` compiled-name escapes for source-facing diagnostics. + let decodeFSharpCompiledName (name: string) = + Regex.Replace( + name, + @"\$([0-9A-Fa-f]{4})", + fun m -> char (System.Convert.ToInt32(m.Groups.[1].Value, 16)) |> string + ) + + /// Erlang caps atoms at 255 characters, but `erlc` builds atoms of its own out of a function's + /// name for the closures, comprehensions and try blocks inside it (`-name/1-fun-0-`), and it + /// crashes in `list_to_atom` rather than reporting an error when one of those overflows. Leave + /// that decoration room. + let private maxErlangAtomLength = 223 + + let private atomNameHash (name: string) = +#if FABLE_COMPILER + // tradeoff: Standalone uses the existing portable hash to avoid cryptography dependencies; overlong atoms differ from native output. + (uint32 (Fable.Naming.stringHash name)).ToString("x8") +#else + use sha256 = System.Security.Cryptography.SHA256.Create() + + name + |> System.Text.Encoding.UTF8.GetBytes + |> sha256.ComputeHash + |> System.BitConverter.ToString + |> _.Replace("-", "") + |> _.ToLowerInvariant() +#endif + + /// Cap an atom at the length Erlang accepts, keeping a readable prefix plus a hash of the full + /// name. Atoms built by joining already-capped parts have to be capped again. + let limitErlangAtomLength (name: string) = + if name.Length <= maxErlangAtomLength then + name + else + let suffix = "_" + atomNameHash name + let mutable prefixLength = maxErlangAtomLength - suffix.Length + + if System.Char.IsHighSurrogate(name.[prefixLength - 1]) then + prefixLength <- prefixLength - 1 + + // decision: Native compilation uses SHA-256 to distinguish overlong names; standalone keeps its portable hash without adding BCL support. + name.Substring(0, prefixLength) + suffix + + /// Drop or encode the characters an F# name may carry that an unquoted Erlang atom cannot /// (`base'`, `op_$0020`, `Foo.Bar`, ...). Every name that ends up as an atom has to go /// through this, or the printer emits something like `'base'_'` — invalid Erlang. let private stripNonAtomChars (name: string) = - // Decode $XXXX hex sequences from F# compiled names (e.g. $0020 -> space -> _) + // invariant: Encoded F# punctuation remains distinct from literal underscores — sanitization must not merge declarations. Regex.Replace( name, @"\$([0-9A-Fa-f]{4})", @@ -80,7 +124,7 @@ module Naming = if System.Char.IsLetterOrDigit(c) then c.ToString() else - "_" + $"_x%s{m.Groups.[1].Value.ToLowerInvariant()}_" ) |> fun s -> s.Replace("'", "").Replace("$", "_").Replace("@", "").Replace(".", "_").Replace("`", "_").Replace("-", "_") @@ -91,19 +135,21 @@ module Naming = |> fun s -> Regex.Replace(s, "_+", "_") |> fun s -> s.Trim('_') |> checkErlKeywords + |> limitErlangAtomLength /// The atom a union case is tagged with at runtime: `[]` verbatim when present, /// otherwise the snake_cased case name. Codegen and the `erl_tag` in reflection metadata must /// both go through this, or reflection looks up a tag the constructor never emitted. let unionCaseTagName (compiledName: string option) (caseName: string) = match compiledName with - | Some name -> name + | Some name -> limitErlangAtomLength name | None -> sanitizeErlangName caseName let moduleNameFromFile (filePath: string) = Fable.Path.GetFileNameWithoutExtension(filePath) |> fun s -> s.Replace(".", "_").Replace("-", "_") |> Fable.Naming.applyCaseRule Fable.Core.CaseRules.SnakeCase + |> limitErlangAtomLength // ---------------------------------------------------------------------------------- // Qualified module names @@ -411,6 +457,7 @@ module Naming = |> fun s -> Regex.Replace(s, "_+", "_") |> fun s -> s.Trim('_') |> checkErlKeywords + |> limitErlangAtomLength /// Number of leading segments `a` and `b` have in common. let private commonPrefixLength (a: string[]) (b: string[]) = @@ -494,7 +541,21 @@ module Naming = else snakeName - checkErlKeywords disambiguated + checkErlKeywords disambiguated |> limitErlangAtomLength + + /// The atom a class or `val` field is stored under in the instance map. Codegen and the + /// collision diagnostic must both derive it here, or a collision is emitted unreported. + let classFieldAtomName (name: string) = + limitErlangAtomLength ("field_" + sanitizeErlangName name) + + /// Derive the map key shared by interface implementations, dispatch, and collision diagnostics. + let interfaceMemberKey isSetter (name: string) = + let memberName = sanitizeErlangName name + + if isSetter then + limitErlangAtomLength ("set_" + memberName) + else + memberName /// Quote an Erlang atom if it needs quoting (starts with uppercase, contains special chars, etc.) /// @@ -565,7 +626,8 @@ module ObjectOverrides = | _ -> false let functionName entityName memberName = - $"%s{entityName}_%s{Naming.sanitizeErlangName memberName}" + let entityName = Naming.sanitizeErlangName entityName + Naming.limitErlangAtomLength $"%s{entityName}_%s{Naming.sanitizeErlangName memberName}" let tryCallZeroArg (com: FSharp2Fable.IFableCompiler) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 5ea619bb6f..6890cdf1e9 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -846,8 +846,11 @@ let private objects | _, Array _ -> physicalEquals r arg1 arg2 |> Some | _ -> equals com r true arg1 arg2 |> Some | "GetHashCode", Some thisObj, [] -> - Helper.LibCall(com, "fable_comparison", "get_hash_code", t, [ thisObj ], ?loc = r) - |> Some + ObjectOverrides.tryCallZeroArg com r t "GetHashCode" thisObj + |> Option.orElseWith (fun () -> + Helper.LibCall(com, "fable_comparison", "get_hash_code", t, [ thisObj ], ?loc = r) + |> Some + ) | "GetType", Some arg, _ -> makeTypeInfo r arg.Type |> Some | "ToString", Some thisObj, [] -> ToString.toStringByType com r t thisObj | _ -> None diff --git a/src/Fable.Transforms/Global/Naming.fs b/src/Fable.Transforms/Global/Naming.fs index 83b0ece01e..3ebd9a41e5 100644 --- a/src/Fable.Transforms/Global/Naming.fs +++ b/src/Fable.Transforms/Global/Naming.fs @@ -534,3 +534,13 @@ module Naming = | c -> sb.Append(c) |> ignore sb.ToString() + + // F# hash function gives different results in different runs + // Taken from fable-library-ts/Util.ts. Possible variant in https://stackoverflow.com/a/1660613 + let stringHash (s: string) = + let mutable h = 5381 + + for i = 0 to s.Length - 1 do + h <- (h * 33) ^^^ (int s[i]) + + h diff --git a/src/Fable.Transforms/OverloadSuffix.fs b/src/Fable.Transforms/OverloadSuffix.fs index 2f353aec58..323543785b 100644 --- a/src/Fable.Transforms/OverloadSuffix.fs +++ b/src/Fable.Transforms/OverloadSuffix.fs @@ -154,19 +154,9 @@ let private combineHashCodes (hashes: int seq) = else hashes |> Array.reduce (fun h1 h2 -> ((h1 <<< 5) + h1) ^^^ h2) -// F# hash function gives different results in different runs -// Taken from fable-library-ts/Util.ts. Possible variant in https://stackoverflow.com/a/1660613 -let private stringHash (s: string) = - let mutable h = 5381 - - for i = 0 to s.Length - 1 do - h <- (h * 33) ^^^ (int s[i]) - - h - let private getHashPrivate (paramTypes: ParamTypes) genParams = paramTypes - |> List.map (getTypeFastFullName genParams >> stringHash) + |> List.map (getTypeFastFullName genParams >> Naming.stringHash) |> combineHashCodes |> hashToString diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index b020d37741..d1c0b7097c 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -449,6 +449,8 @@ let transformToTargetAst (com: CompilerImpl) (fableAst: Fable.File) : IFableResu upcast RustResult(ast, errors) | Beam -> let ast = Fable.Transforms.Beam.Compiler.transformFile com fableAst + // Naming diagnostics are raised while constructing the target AST, after the earlier snapshot. + let errors = com.Logs |> Array.map (mapFableError com) upcast BeamResult(ast, errors) let compileToTargetAst (results: IParseAndCheckResults) fileName fableLibrary typedArrays language : IFableResult = diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 1696641081..6d414a464f 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -142,6 +142,31 @@ if (subsetStats.Fable_transform >= stats.Fable_transform) { failures.push(`emitting 1 of ${fixture.length} files took as long as emitting all of them`) } +// BEAM names in the web compiler use its existing hash, with no cryptography metadata. +const longFunction = "long" + "x".repeat(230) +post(["CompileFiles", [{ Name: "Program.fs", Content: `module Demo\nlet ${longFunction} () = 42` }], [], "beam", []]) +const [, beamModules, , beamErrors] = await answer("CompilationsFinished") +const beamHard = (beamErrors ?? []).filter((e) => !e.IsWarning) +if (beamHard.length) failures.push(`BEAM example failed: ${beamHard[0].Message}`) +const beamCode = beamModules.join("\n") +if (!/longx+_[0-9a-f]{8}\(/.test(beamCode)) { + failures.push("standalone BEAM example did not use the existing 32-bit hash") +} + +// A collision in a state key must be diagnosed even if the unprefixed names remain distinct. +const mutablePrefix = "a".repeat(220) +post(["CompileFiles", [{ Name: "Program.fs", Content: [ + "module Demo", + `let mutable ${mutablePrefix}a2 = 1`, + `let mutable ${mutablePrefix}cp = 2`, + `let read () = ${mutablePrefix}a2, ${mutablePrefix}cp`, +].join("\n") }], [], "beam", []]) +const [, , , collisionErrors] = await answer("CompilationsFinished") +if (!(collisionErrors ?? []).some((e) => !e.IsWarning && e.Message.includes("share Erlang state key"))) { + failures.push("standalone BEAM did not diagnose colliding mutable state keys") +} +console.log(" BEAM example hashing and state-key diagnostics checked") + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) diff --git a/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj b/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj new file mode 100644 index 0000000000..705031e61e --- /dev/null +++ b/tests/Beam/CollidingMemberNames/CollidingMemberNames.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + false + preview + + + + + + + + diff --git a/tests/Beam/CollidingMemberNames/Program.fs b/tests/Beam/CollidingMemberNames/Program.fs new file mode 100644 index 0000000000..7dd9454bac --- /dev/null +++ b/tests/Beam/CollidingMemberNames/Program.fs @@ -0,0 +1,51 @@ +module Fable.Tests.CollidingMemberNames + +type Foo() = class end + +let fooBar () = 1 +let foo_bar () = 2 +let foo_ctor () = 42 + +let values () = fooBar (), foo_bar (), foo_ctor () + +// Members differing only in case are legal F# but collapse to one Erlang atom. Unlike a captured +// `let`, another file can call them, so they must be reported rather than silently renamed. +type Renderer() = + member _.work() = 1 + member _.Work() = 2 + +let render () = + let r = Renderer() + r.work (), r.Work() + +// Class and `val` fields are keyed by `classFieldAtomName`, which collapses case and runs of +// underscores. Two fields that differ only there share one map key. +type CtorFields(fooBar: int, FooBar: int) = + member _.Lower = fooBar + member _.Upper = FooBar + +type ValFields() = + [] + val mutable fooBar: int + + [] + val mutable FooBar: int + +// Union tags and interface dispatch keys are atoms too. A shared tag makes `match` pick the wrong +// branch and two values compare equal; a shared key makes one member resolve to the other's body. +type CollidingUnion = + | FooBar + | Foo_Bar + +type CompiledNameUnion = + | [] Tagged + | Foo_Bar + +type ICollidingMembers = + abstract FooBar: unit -> int + abstract Foo_Bar: unit -> int + +// The prefixed setter key is bounded again, so it can collide with an explicitly named method. +type ILongSetterCollision = + abstract pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp: int with get, set + abstract set_pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp_eb2801e2b9e2ecf71459fa6e18bf811861522adcea87b798f928b62a2242245c: unit -> int diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index ffe0e9a08b..2b14e17b7b 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -30,6 +30,12 @@ let log2 (a: string) (b: string) = String.Format("a = {0}, b = {1}", a, b) let logItem1 = log2 "item1" let logItem2 = log2 "item2" +let ``punctuation-name`` () = 1 +let punctuation_name () = 2 + +let ``abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh`` () = + 3 + type PartialFunctions() = member _.logItem1 = log2 "item1" member _.logItem2 = log2 "item2" @@ -444,6 +450,15 @@ let ``test Conversion to Action works`` () = f3'.Invoke(10) equal 40 myField +[] +let ``test module functions with punctuation remain distinct`` () = + (``punctuation-name`` (), punctuation_name ()) |> equal (1, 2) + +[] +let ``test escaped function names stay within the Erlang atom limit`` () = + ``abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh abcdefgh`` () + |> equal 3 + // -- Object Expressions -- [] @@ -488,15 +503,13 @@ let ``test Composition with recursive this works`` () = RecursiveType(fun f -> x <- f()) |> ignore equal 11 x -// TODO: Re-enable when Beam target disambiguates local let bindings from same-named members -// (MyComponent has `let work i = ...` and `member _.Work i = work i` which collide after sanitization) -// [] -// let ``test References to enclosing type from object expression work`` () = -// MyComponent("TestA").works1().doWork() |> equal "TestA-1" -// MyComponent("TestB").works2().doWork() |> equal "TestB-2" -// MyComponent("TestC").works3().doWork() |> equal "TestC-3" -// MyComponent("TestD").works4().doWork() |> equal "TestD-4" -// MyComponent("TestE").works5().doWork() |> equal "TestE-5" +[] +let ``test References to enclosing type from object expression work`` () = + MyComponent("TestA").works1().doWork() |> equal "TestA-1" + MyComponent("TestB").works2().doWork() |> equal "TestB-2" + MyComponent("TestC").works3().doWork() |> equal "TestC-3" + MyComponent("TestD").works4().doWork() |> equal "TestD-4" + MyComponent("TestE").works5().doWork() |> equal "TestE-5" [] let ``test Inlined object expression doesn't change argument this context`` () = diff --git a/tests/Beam/ModuleNamingTests.fs b/tests/Beam/ModuleNamingTests.fs index 1ff43a4717..f84f7f92ed 100644 --- a/tests/Beam/ModuleNamingTests.fs +++ b/tests/Beam/ModuleNamingTests.fs @@ -31,3 +31,107 @@ let ``test Module named String does not resolve to OTP string`` () = let ``test Same-named files in different directories both survive`` () = Naming.First.Types.area (Naming.First.Types.Circle 2.0) |> equal 12.0 Naming.Second.Types.name Naming.Second.Types.Red |> equal "red" + +// --- Names longer than Erlang's 255-character atom limit --- + +// Both names are 263 characters and share their first 260, so the truncated prefix alone +// cannot tell them apart; an overlong atom fails in `erlc`, a merged one calls the wrong body. +let private overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxone () = 1 + +let private overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtwo () = 2 + +[] +let ``test Names over the atom length limit stay valid and distinct`` () = + overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxone () |> equal 1 + overlongfunctionnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxtwo () |> equal 2 + +// The atom limiter caps each sanitized name at 223, so anything joined onto a capped name +// (`_reflection`, `_`) overflows the limit again unless re-capped. +type Overlongrecordnamerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrend = { Value: int } + +type Overlongclassnamecccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccend(value: int) = + member _.Overlongpropertynameppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppend = value + +[] +let ``test Atoms composed from capped names stay within the limit`` () = + let record = { Overlongrecordnamerrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrend.Value = 1 } + record.Value |> equal 1 + Overlongclassnamecccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccend(2).Overlongpropertynameppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppend |> equal 2 + +// A `[]` is used as the runtime tag verbatim, so it has to be capped like any +// other atom. Codegen and the reflection `erl_tag` must agree on the capped form. +type OverlongTag = + | [] Tagged + | Other + +[] +let ``test Overlong CompiledName union tags stay valid and distinct`` () = + Tagged = Other |> equal false + Tagged = Tagged |> equal true + + match Tagged with + | Tagged -> () + | Other -> failwith "wrong case" + +// These names collide under djb2 once the emitting module name is added to the state key. +let mutable private aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 = 1 +let mutable private aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp = 2 + +[] +let ``test Bounded mutable state keys preserve distinct values`` () = + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 |> equal 1 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp |> equal 2 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 <- 11 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp <- 22 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2 |> equal 11 + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafp |> equal 22 + +type ILongProperty = + abstract pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp: int with get, set + +type LongPropertyClass() = + let mutable value = 0 + + interface ILongProperty with + member _.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp + with get () = value + and set v = value <- v + +[] +let ``test Long interface setters work on object expressions`` () = + let mutable value = 0 + + let instance = + { new ILongProperty with + member _.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp + with get () = value + and set v = value <- v + } + + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp <- 42 + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp |> equal 42 + value |> equal 42 + +[] +let ``test Long interface setters work on classes`` () = + let instance = LongPropertyClass() :> ILongProperty + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp <- 42 + instance.pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp |> equal 42 + +type Ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc() = + override _.ToString() = "ok" + override _.GetHashCode() = 42 + +[] +let ``test Long override entity names resolve consistently`` () = + let instance = Ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc() + string instance |> equal "ok" + instance.ToString() |> equal "ok" + instance.GetHashCode() |> equal 42 + +type HashOverride() = + override _.GetHashCode() = 43 + +[] +let ``test Direct hash code calls use class overrides`` () = + HashOverride().GetHashCode() |> equal 43 diff --git a/tests/Beam/RecordTests.fs b/tests/Beam/RecordTests.fs index fe296cfdbd..8d13df6530 100644 --- a/tests/Beam/RecordTests.fs +++ b/tests/Beam/RecordTests.fs @@ -18,8 +18,9 @@ type JSKiller = type ErlangKeywordRecord = { Receive: string; Case: int; Fun: bool } -// Note: Records with spaces/symbols in field names are not supported in Beam -// as Erlang atoms can't contain these characters +type SpecialCharacterRecord = + { ``s p a c e``: float + ``s*y*m*b*o*l``: float } type MutatingRecord = { uniqueA: int; uniqueB: int } @@ -129,7 +130,14 @@ let ``test records with Erlang reserved word fields work`` () = equal "world" r2.Receive equal 42 r2.Case -// test records with special characters - skipped for Beam (Erlang atoms can't have spaces/symbols) +[] +let ``test records with special characters are mapped correctly`` () = + let x = + { ``s p a c e`` = 1.0 + ``s*y*m*b*o*l`` = 2.0 } + + equal 1.0 x.``s p a c e`` + equal 2.0 x.``s*y*m*b*o*l`` [] let ``test mutating records work`` () = diff --git a/tests/Beam/UnsupportedNumericLiteral/Program.fs b/tests/Beam/UnsupportedNumericLiteral/Program.fs index ce52d638be..04e12ac1d3 100644 --- a/tests/Beam/UnsupportedNumericLiteral/Program.fs +++ b/tests/Beam/UnsupportedNumericLiteral/Program.fs @@ -1,3 +1,9 @@ module Fable.Tests.UnsupportedNumericLiteral let value = Unchecked.defaultof + +type CollidingRecord = + { ``foo-bar``: int + foo_bar: int } + +let collidingAnonymousRecord = {| ``foo-bar`` = 1; foo_bar = 2 |} From b32a8bd9caae4dcb7b7be4670793b3e7a54ad741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:02:26 +0200 Subject: [PATCH 134/322] chore: Bump ruff from 0.16.9 to 0.16.10 in the all-pip group (#5048) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27357ae6e5..5f63cbdb5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.9,<0.17", + "ruff>=0.16.10,<0.17", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", "pydantic>=2.13.5", diff --git a/uv.lock b/uv.lock index b1b657365f..445b7b96b9 100644 --- a/uv.lock +++ b/uv.lock @@ -68,7 +68,7 @@ dev = [ { name = "pydantic", specifier = ">=2.13.5" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.9,<0.17" }, + { name = "ruff", specifier = ">=0.16.10,<0.17" }, { name = "ty", specifier = ">=0.0.84" }, ] @@ -259,27 +259,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.9" +version = "0.16.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, - { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, - { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, - { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, - { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, - { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, - { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, - { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, - { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, - { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, - { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, ] [[package]] From d5eaee09e716940d63b5b9107db52e937c18a7e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:03:00 +0200 Subject: [PATCH 135/322] chore: Bump pyo3 from 0.29.2 to 0.29.3 in /src/fable-library-py in the all-cargo group (#5044) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/Cargo.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/fable-library-py/Cargo.lock b/src/fable-library-py/Cargo.lock index 25c174c73b..6473d44bc9 100644 --- a/src/fable-library-py/Cargo.lock +++ b/src/fable-library-py/Cargo.lock @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" +checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655" dependencies = [ "chrono", "libc", @@ -194,18 +194,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" +checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" +checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330" dependencies = [ "libc", "pyo3-build-config", @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" +checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.29.2" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" +checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3" dependencies = [ "heck", "proc-macro2", From a7f4fc592a41512c9c6c8ab91b7c0d8e5bdee2e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:03:26 +0200 Subject: [PATCH 136/322] chore: Bump the all-actions group with 2 updates (#5046) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b8ef51ee27..4319e2cf67 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup-cli@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index 0348c9f1ca..bf5d9b5b20 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@2fbab69bfca02bebd76cd0fc43f2d12acfed994f # v0.89.22 + uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From 4fad3395d9361e554a493d9c996a18dd3e164d89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:05:12 +0200 Subject: [PATCH 137/322] chore: Bump hypothesis from 6.168.2 to 6.168.3 in /src/fable-library-py in the all-pip group (#5047) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 172 +++++++++++++++++------------------ 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index 24c90c9a1c..ec6c98dbc3 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,98 +61,98 @@ dev = [ [[package]] name = "hypothesis" -version = "6.168.2" +version = "6.168.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/10/5795cc413d2ba5ff9c81e5d1b27ba3c652ac6dfac6b1d67072052d338391/hypothesis-6.168.2.tar.gz", hash = "sha256:df61fcada928a3fb3fba4602be5b2d569eb50c36fddae0d2e2a5f51419d0ce9b", size = 510987, upload-time = "2026-09-27T05:22:19.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/8a/1a244014d639f3d21e419a8207b7f1c2d8f4caf6e14073c75bfb364b27e3/hypothesis-6.168.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:14d857ee2669e76d7aee7fa56a2f13d40fc6ba8a34c7b340fd53744a7623f9d3", size = 791551, upload-time = "2026-09-27T05:19:59.476Z" }, - { url = "https://files.pythonhosted.org/packages/17/47/fa17a1a3cc971bf454d2b4fd0ccc2d35ab2d4b705efbc5e4149b70aa4072/hypothesis-6.168.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1ca4205f8bff16383a35924ec6b87ec649a407d204c9377da225924d695aab23", size = 787291, upload-time = "2026-09-27T05:21:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/a5/79/989e20cebec9fef907c43e34dae9c0938b49e2d3bb8537a79990692471c0/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8268bcc733f2b93e77f64e54d6fa61db79af91806e2d26b950f5d1cca47b4bfa", size = 1124033, upload-time = "2026-09-27T05:20:17.522Z" }, - { url = "https://files.pythonhosted.org/packages/ec/44/3b5edc918128eccea902eb6ee91f0c58ad575274b99c565107fc575483a9/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c138d9848c210b8a1e511ab0884f16bc71470400b11255e8b595df320809afc", size = 1147868, upload-time = "2026-09-27T05:20:44.835Z" }, - { url = "https://files.pythonhosted.org/packages/2a/54/04d30544681626fb7fa25cc1041c12ebfad79aa4995438f6f3e577541111/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:420479d419d73dd7bb2ed0cfb57f35b1dc0bc246c30b250904c3987c5cae9463", size = 1149472, upload-time = "2026-09-27T05:21:49.503Z" }, - { url = "https://files.pythonhosted.org/packages/14/86/a7abe9660b4b997305537aa860fb932f97223125713655a7a1e9a302f629/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01f74944abe6988a6213dd036bd2dbf1e4f0513af0cf66b7780773fdf0c6f5e2", size = 1191885, upload-time = "2026-09-27T05:22:15.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/24c77c25ec62941022edcbb98a052b98c57b60fee7d289cabea73523a727/hypothesis-6.168.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe84b8218a4fc33f79e64840d23fe9c627e7acb7cad1f70bf6c0f163c7f4f01f", size = 1169899, upload-time = "2026-09-27T05:20:13.379Z" }, - { url = "https://files.pythonhosted.org/packages/cb/23/dd129e570f44051de911761cada83db5695bbbe3db450bc905d449ea8aa2/hypothesis-6.168.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:ce74728a05d5e5dd4d28bb4fbaa0c1f5e809ebf6be54d343128d970ee1025697", size = 1129354, upload-time = "2026-09-27T05:20:10.768Z" }, - { url = "https://files.pythonhosted.org/packages/71/65/6d911e5b270c7f9560929b07304869a1f77e2440e22db107600788975bbb/hypothesis-6.168.2-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29e12bcfcef363a9dbd0d3d62fd9608853268d15319ae3ecaf802555497392a9", size = 1160386, upload-time = "2026-09-27T05:22:05.475Z" }, - { url = "https://files.pythonhosted.org/packages/26/87/5cb26f7a2323ecdb4881f06c8eec19c4087e04b23cc7998b74f2b565a66c/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3cbe12b2427a86d769fdc93242ee3207d3a1523798b856630f16b674ebae17b1", size = 1299894, upload-time = "2026-09-27T05:20:02.55Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f3/eb40e5c554b78b1b500b93172829b4b16c677acadd3f10f979117a82f851/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3b62ce43973f4425530e590751439bacceec19921af0edf9fae3a63e173ea146", size = 1425524, upload-time = "2026-09-27T05:20:58.58Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1e/03819636fa86ade2db91ad9953851b49f885a3481a22f0b1361d0c5dfc8d/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:51df0068106dd2db4fc5a1e56801adf5fc1d354792584289a9cc8ac09273d376", size = 1377070, upload-time = "2026-09-27T05:21:04.963Z" }, - { url = "https://files.pythonhosted.org/packages/5b/80/505146f225bbd24759651a16c17cd44aa6f082fa4a14761646e8ec37f374/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:e54638a0f66c608840af13f3ad665710e706bda5c52f706351bb83b5744bec63", size = 1281230, upload-time = "2026-09-27T05:20:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7b/3c9f0f1cf0733b6d692dd5dca92037f6b707082bfb4c29712f2d0258049e/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:84c5b9891f979e851d140618783e9c3624352760dd78cba4759b263686cdad16", size = 1300413, upload-time = "2026-09-27T05:20:14.695Z" }, - { url = "https://files.pythonhosted.org/packages/81/dc/56c1626009545b86fefa71ee71d273fe518d1374beca40bc9b784818a58e/hypothesis-6.168.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6a6ac0836daba2da486a4fefc22fcdd794dac350bd787a1bb896748cdc47e05e", size = 1336267, upload-time = "2026-09-27T05:21:23.381Z" }, - { url = "https://files.pythonhosted.org/packages/ea/44/4b4b58350d93f910165b6b799e757735d0880f624a189b32e8cd4214d8cf/hypothesis-6.168.2-cp310-abi3-win32.whl", hash = "sha256:ac8e27fa3b5a97d692db528a808678181d931056c76cc7901c22329a58ed5e71", size = 678191, upload-time = "2026-09-27T05:21:18.427Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c2/f7e7f3c895df3182f78bca8e3564fa169d8f5b2ee2880a6394bc8c86f22c/hypothesis-6.168.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e9a03e9c1a725118986ab8b65a7b9ef090fa6e88e882897e4f8351865a53834", size = 684899, upload-time = "2026-09-27T05:21:38.328Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f1/2d215f89caf7b881f8e7eac4796e3f7080d0324d8f62d782e0becf5df1d8/hypothesis-6.168.2-cp310-abi3-win_arm64.whl", hash = "sha256:60212566e2c4877f3aa9f5c754b07cb1f087c943fdf91cd13e28fd490b3099b3", size = 682893, upload-time = "2026-09-27T05:21:20.039Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/1aafd1d38294bf0ba630218aad14b810e96ab3cad46753119abf75740cbd/hypothesis-6.168.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:656ecb3028fc88dd5714592de3bd6dce68224b1387e7664d96854ea10f7d8140", size = 792310, upload-time = "2026-09-27T05:21:08.548Z" }, - { url = "https://files.pythonhosted.org/packages/6b/5b/c6ccc50a5d76d22a89b3fa639d3a903007c1d210824204e651ee455e4770/hypothesis-6.168.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3be0148392a9326436ed656ecb4ee646a78213a5e391aecbbdc9226cf4532a28", size = 788321, upload-time = "2026-09-27T05:20:37.249Z" }, - { url = "https://files.pythonhosted.org/packages/de/9b/5a7f40ce83d41d4bc82ad699c132b4289e949784bb7e4a4172fb32395eb2/hypothesis-6.168.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371a9d749dac3316697b099dad274a6f9d9ea860b73ccdef4936859fa9dbb8e", size = 1124421, upload-time = "2026-09-27T05:22:11.307Z" }, - { url = "https://files.pythonhosted.org/packages/cd/fb/285c816766713c0af37dbcbe99e9e3c81259a2a3ac3c84fa6fc2348add6c/hypothesis-6.168.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c5c743ab68f567f7be51d402301121a10dddffbdcbd3072282035b80f8a7bb", size = 1170623, upload-time = "2026-09-27T05:22:07.462Z" }, - { url = "https://files.pythonhosted.org/packages/51/69/9cb54cd41874ce8dd7e7879ccac5aa5ea1a46e5857ad50df8e2a85cf8bc5/hypothesis-6.168.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68920b82b53f9318b7c881b2386bad1b2437f1f79d4681363c6c41a81b9d62f3", size = 1300440, upload-time = "2026-09-27T05:22:01.25Z" }, - { url = "https://files.pythonhosted.org/packages/d3/14/9af1bee77064d075e8adc25a48bd967a5eb0f7807d088a9a3a1937b7aafb/hypothesis-6.168.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1e17436910f43db278363571d92e72a4a45f6e2552919a154a50f3b3f2120c4", size = 1336902, upload-time = "2026-09-27T05:20:34.001Z" }, - { url = "https://files.pythonhosted.org/packages/bf/02/a98f38cc794717476380d28868911e42a80f3f49a2505235f51e9e4a4e51/hypothesis-6.168.2-cp310-cp310-win_amd64.whl", hash = "sha256:69a5492a45500afc7e0263678cf872a3df4234075a8370c45348b2e6282735ae", size = 684844, upload-time = "2026-09-27T05:20:12.132Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6f/d73a4e028552df5699eae91af65da0442ee494846ead5c394682a19e22f9/hypothesis-6.168.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:502a2a9e72f20bc61071713d140f0cb21f50ca8736d9286077af0d00c38c7541", size = 792224, upload-time = "2026-09-27T05:22:09.37Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f4/b6d21f72d4e38762d6ec56fc6e8739b78e13904a4790c4f10ffda39daa17/hypothesis-6.168.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f074d433004135f6c03047da508701481ebdb0c0829be3f0e8e20379c05232a", size = 788103, upload-time = "2026-09-27T05:21:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/60/c0/f24cec4cdeb1fc1c1c8e6aa06f0e8f871e03f12afd2808279ab36fdfe0f0/hypothesis-6.168.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d193d435cf3fa604f1be94ecfa2936bf6ed78db33cfb7da84e9feb57b45e12", size = 1124229, upload-time = "2026-09-27T05:21:36.634Z" }, - { url = "https://files.pythonhosted.org/packages/5f/1f/5475eb96154d20160bf62c74076b5b56744dcb8984836dce6e068a5028e9/hypothesis-6.168.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21d98055721e8ca1604dbcac18f7329bec4793cbf14b21081c3cdf471db9255e", size = 1170405, upload-time = "2026-09-27T05:20:31.066Z" }, - { url = "https://files.pythonhosted.org/packages/91/ba/f47e5adf68cbcb1c89a75854a3d5048e9ad51c7daec60b676ed2a27fef76/hypothesis-6.168.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:584979873b2bd375a4194252233b12cb237ee57b28acd6c1aa846fc9d9dea85f", size = 1300335, upload-time = "2026-09-27T05:20:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/e0/85/b717012e1f7eed35fa7b10f75674b33774ddc8736fd1bb965f22d8e35abe/hypothesis-6.168.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ec125fb3b07ec8f76160d002c1b4a023c9467e76ba0dcbe9368d51e352bd460", size = 1336470, upload-time = "2026-09-27T05:20:41.909Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/1e1f9fb5fd45dd16aff5cc38f5428150be751a0ee0ff225f1fcbbb79c1e9/hypothesis-6.168.2-cp311-cp311-win_amd64.whl", hash = "sha256:54a76da7694a8c759bb9118523584942d718a7dbb61a321eaf7f085467ae31fd", size = 684657, upload-time = "2026-09-27T05:20:43.328Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f2/c398a808363b3661bb0acd5e3103b7203262e63acef5ccf2acccbad041cb/hypothesis-6.168.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:47064622758ed066ff39bca51ae2dbe7936be9369fb04da7a661b8beea489501", size = 793299, upload-time = "2026-09-27T05:20:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/3c/57/822d48fa61e9bd3049ce5caab399350fafa5f592e8a89bf53fabf798b14e/hypothesis-6.168.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1fdf95461ef7c745d9711a9ac865dc7557bb53a1cc973d2a8f54978ac6df45b6", size = 784846, upload-time = "2026-09-27T05:20:57.024Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/853cc464739bfd076e0f6bac55c4fcff362effdcfc10873bd1a119e80994/hypothesis-6.168.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e28cb8912d7811dcec91b2389d9dcf3b34523c043cfb7454f9c62c8e5a40b2a", size = 1123030, upload-time = "2026-09-27T05:20:38.642Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6a/506d3c0e45bf8cfb6bce2842b0642df37db07f2d5695f4614fa410e854cc/hypothesis-6.168.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3630d664a343354701618a409d3b9d563cb97cfcf749c7a37c5fb32cb3c7084c", size = 1169073, upload-time = "2026-09-27T05:20:55.361Z" }, - { url = "https://files.pythonhosted.org/packages/82/52/244eff24dbaa1f9520dcb42164fb3769e6c08a2e252455af2e8bd23305e5/hypothesis-6.168.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0df395d27012b07eeed98b2e3f2a17f693236b0518d8a086679dc657801aead", size = 1298741, upload-time = "2026-09-27T05:20:32.609Z" }, - { url = "https://files.pythonhosted.org/packages/2e/99/cdcb4fa1b738ef07827464bdf5ff83897f8303e8eca569207cc52f5be2b2/hypothesis-6.168.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:778f043b8af618baabe79cdb15f6f7855ccf950c6911eea84acabca5b7637cde", size = 1335252, upload-time = "2026-09-27T05:20:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f2/09c4e5a334d89d242e9068106cd3286f45f642a7658397bab367c5a9a267/hypothesis-6.168.2-cp312-cp312-win_amd64.whl", hash = "sha256:66d0187f7d81cc4d016e211a8c042b1ac169685ce6ea265acff04e299e189d4f", size = 682188, upload-time = "2026-09-27T05:20:52.329Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d2/64be17ff67f5178b935efac1154f2251fae223b11529431e7e444407041d/hypothesis-6.168.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5c74b4fdd7945130b825018e71515b0b9042ab726ef939dd70984138dfc4eb2c", size = 793237, upload-time = "2026-09-27T05:21:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/8f/09/4e5096fc2b85843abcd7f30e92880fd3a0d08d166c56b468adcdc5cf9c5b/hypothesis-6.168.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37747c599a586ab53819404f4581624290da7f374f892c7f572f9921e90abe00", size = 784689, upload-time = "2026-09-27T05:21:11.902Z" }, - { url = "https://files.pythonhosted.org/packages/a5/49/63f0a6b5b709e2bdd0d1d0041c48c6d4614323e27c677a0d44d9748dd4cb/hypothesis-6.168.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97437cf297feb9645c23fd187734c137989c503303542a4034eb43d07f8f151e", size = 1123008, upload-time = "2026-09-27T05:20:01.27Z" }, - { url = "https://files.pythonhosted.org/packages/a5/21/284046912c05973f91d41bd7ed79e3bf1917b1332b70e677c3cf2872cea8/hypothesis-6.168.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5bd1b6334eaccde40ab56ffb96788f2af993869b08aebc4f1b3ff28f3edcda9", size = 1168965, upload-time = "2026-09-27T05:20:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b7/afe659dccd780393ca6beceabc3a418f11ff2539a83bc4ce9e6c8a0ab589/hypothesis-6.168.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac134d9fe4a1623b57b2a1681e3f51c2c5b324e690c1264dc01a3c57c445b08a", size = 1298607, upload-time = "2026-09-27T05:20:21.424Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/d6300216c75fe878da9bc8bfc5e266baa1d350ea145001272d20b3ff3843/hypothesis-6.168.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ba2a0767955fd6491cb93e6917db1d5b718a89f9a4de33b0827b7fce1df289", size = 1335166, upload-time = "2026-09-27T05:20:53.745Z" }, - { url = "https://files.pythonhosted.org/packages/f5/fe/ab19f96bf248571b6e7aef051673c8d7e3b01cba4c40c5f0e2a4be3df398/hypothesis-6.168.2-cp313-cp313-win_amd64.whl", hash = "sha256:9746ac4e78a25be801de86aa386d6e2f95dd6d6b88ce4a1ad639b3d009425fae", size = 682154, upload-time = "2026-09-27T05:20:25.283Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/c1b7b533845575f84fa8c20343bf67340bc2d9f19ec8c457cdbb886aaeee/hypothesis-6.168.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:515e0fcad7b3cfc81c93b5d27024b98f5e53506394373fd7e319ee303f294aa9", size = 793273, upload-time = "2026-09-27T05:21:57.508Z" }, - { url = "https://files.pythonhosted.org/packages/7d/76/060863fcdba95cdc1390719034bb767bfde00f399a3763b2fdcac65137d9/hypothesis-6.168.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b55bd7d4af2cea659df6660266d4a70ad947d668685c386d79cc9884a30c17f3", size = 784836, upload-time = "2026-09-27T05:20:35.57Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2a/11e427ed3f09f0e1d548428144e97fb9bfbdb2f41b6871880625acf41411/hypothesis-6.168.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db13338672ba689203f405c6258ef62b0590e3d193714b30809e53e8bfc66e84", size = 1123318, upload-time = "2026-09-27T05:21:30.573Z" }, - { url = "https://files.pythonhosted.org/packages/89/03/80911aca76e411cb1d1e7a5deaea0a49b47321ccd85092ceba971100b921/hypothesis-6.168.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:170a6d2d6d9d209992db1e671d0c2ad17367c8046482ebff2cbc9e14a9698be7", size = 1169146, upload-time = "2026-09-27T05:20:20.159Z" }, - { url = "https://files.pythonhosted.org/packages/b3/19/243f18213e39924db4be21b53e3999c183d12954745254e717a639fc3490/hypothesis-6.168.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9310d3f997f3d5e8e31d5c5f9946bcd3ccf6eb62c92a1b728f2dd57fd62fda05", size = 1299106, upload-time = "2026-09-27T05:21:06.885Z" }, - { url = "https://files.pythonhosted.org/packages/6a/47/abc5881cf0cdce97663b877f486ff064d834850ea19171ba556c7dd835ae/hypothesis-6.168.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d66e68a0dc9e216f9bdda2c9f066c09e36849cbcb49fbcb1860087560709bb8d", size = 1335374, upload-time = "2026-09-27T05:20:46.633Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f9/08d4bba84eab0404b42971af4ee21c4f9f2064444dc7c8040a14f722ffea/hypothesis-6.168.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f98f7581f68acc659510f8786c39c5cf140dbc3dacba6cd0ed059b0e296a158b", size = 624273, upload-time = "2026-09-27T05:20:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/2b8941a0407da26cf1f789f235cd5dc0e5958b80b7bc12c94596bd24abc0/hypothesis-6.168.2-cp314-cp314-win_amd64.whl", hash = "sha256:6732958300f8994f4b81df11fecf66671f8dd5899de99c3a57958d12e2409e65", size = 682037, upload-time = "2026-09-27T05:21:21.707Z" }, - { url = "https://files.pythonhosted.org/packages/2b/72/6c87e74d5763512fcdd4da92654c611529ddead00b5eafe7265640cd3b8f/hypothesis-6.168.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:20b5024b215208847e5bd2003a89110b74031bfa218d05b10edeb8b8c1354c42", size = 791861, upload-time = "2026-09-27T05:22:03.303Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d8/d6df42f5fa6867a3aeadf372743f2def8d1c4444ce48a06a0250954c5bec/hypothesis-6.168.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7d052c2770ba607e00f7a40965021ceefcd676ee459a1e44daa26ed61d56f32", size = 783409, upload-time = "2026-09-27T05:22:17.92Z" }, - { url = "https://files.pythonhosted.org/packages/bb/e0/92fc641a751af530859023ea6ae6f73c2294587240b78884aa155fccbe5a/hypothesis-6.168.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798ae9a91ab4cd8cfadd7991f46dc9c12fd43d2997f29744b7eae755ff36027a", size = 1121651, upload-time = "2026-09-27T05:21:40.104Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1a/405c4c9b888721fc3b650ffc3c86d8507450e1ea105e88c97e2e5f277aa6/hypothesis-6.168.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00f8728aaef10c91688dab34df8a59aed83c76ade869b1812f01ac80808bc5cc", size = 1167748, upload-time = "2026-09-27T05:21:41.918Z" }, - { url = "https://files.pythonhosted.org/packages/b1/70/2f7559b159f98e14d950914a0aeb25b0e2ed473f35705192de4a2c20b3e8/hypothesis-6.168.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:63b183d5436688e54389f54b66d1611aef349f36345b6dc0a79ccef0dbc736c8", size = 1297293, upload-time = "2026-09-27T05:21:03.359Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c3/b197f8353c3026b76da82d49a119849cd9eac7b0be96239414d1d748156c/hypothesis-6.168.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:28dfe52bcf6e6621153cfbb5574223d117a3e66b24ffeacb4c9bc8f751190dc6", size = 1334246, upload-time = "2026-09-27T05:21:27.001Z" }, - { url = "https://files.pythonhosted.org/packages/65/eb/555178285b606bad3e1127868500b064822574e409618f0e88c17cb63242/hypothesis-6.168.2-cp314-cp314t-win_amd64.whl", hash = "sha256:4aa407c3b0b1d7d980088e6aeb634a53cca879d68ffaf1efdb284b2fc70441e4", size = 681980, upload-time = "2026-09-27T05:21:53.418Z" }, - { url = "https://files.pythonhosted.org/packages/a6/0b/30c6ca63dfe1c372670df30f7fd0f42efe890c13def205c9c4ca48eea716/hypothesis-6.168.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:567ced8b7eca653544a70f0e44c381a6cbe151c6a6544119e39fb9d2f43c3b4c", size = 791262, upload-time = "2026-09-27T05:21:10.337Z" }, - { url = "https://files.pythonhosted.org/packages/87/3f/373a33a0e85fc74fb2a525023a4d163b29ca5ff60f6cc106ca205324dffe/hypothesis-6.168.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:c1fb42a01f8b6d235887787772d6969453440d3d4a6bf52be6831a720f4caf25", size = 783152, upload-time = "2026-09-27T05:21:45.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/98/55f83a1a9cb643866c27211a991cf56a012126a6d5548a02877df1334ab0/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3001d9e6013ebb82d9d7daa97efd75a383e628bf2b5d754512b56191898e377", size = 1121134, upload-time = "2026-09-27T05:21:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/87/dd/2a642db35341893ee604d8cd0b8e6232ddc848e9fe04d6a17fd30cdbc586/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44b7c3d431cca39342881761dfe3e34b39c9b5e7b0ad5c8c4cb7d92514f3b94c", size = 1144046, upload-time = "2026-09-27T05:20:06.616Z" }, - { url = "https://files.pythonhosted.org/packages/61/13/78e7b3192d4ccd19bbf36ebc0a678a8291f40093baee47fdded4240d2ca7/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78ce758211e2fd049c358c979c0c1312fc0f6e13a829cf2a4828a9ebb1423f8", size = 1146678, upload-time = "2026-09-27T05:21:34.447Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6a/daa33a3e9eb999da96bd7315d1f5446c5e28866191e2007aad9b1fb6616f/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9aec441b5d7156e64285817fda72b0aad37aa3dd262693708e825858930b7b48", size = 1189433, upload-time = "2026-09-27T05:21:55.422Z" }, - { url = "https://files.pythonhosted.org/packages/28/49/138fa2c61ec12ea7b17a1693a17ce0567ede4e61dac65d3304664493641b/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3510ca51dfd5aa7eb24f39d65714f6a91297fa5382866124b931bf56acd207fd", size = 1167045, upload-time = "2026-09-27T05:22:13.561Z" }, - { url = "https://files.pythonhosted.org/packages/e5/04/38474d9c6bc119700b6eb6fdb73ff8775202af79d5a7ee3bd06619d1b5a0/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d7943a4c8dcca314bfd918247acea4a36fd3476167372b19d8db3070cdefa4f", size = 1126814, upload-time = "2026-09-27T05:21:43.835Z" }, - { url = "https://files.pythonhosted.org/packages/99/be/048888e2f1bfe412c46788443079bb03b6c299c0da85f11fdb536f27dfa4/hypothesis-6.168.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:903cc76f9ebd36d555039aac270f5652cf0a93cfbbac344915ee3529e917efa3", size = 1155839, upload-time = "2026-09-27T05:20:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/3a/67/0a607ac12fedbd8b2be26b360013df3b064e6b80d7313db635bfb5cc75de/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d6f060f4bcf10ac087c34da08fc9f3be979dcfb70d45a238a6fe6298f1cc1ae9", size = 1296640, upload-time = "2026-09-27T05:21:28.848Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f8/f59636b8b9b8c6de5f02b752faba3c7592a13dd767ddc96bf582b8870720/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:8574573eea6693792966eb6c8da079fe63d8dafb884c613c9857b3a13052df45", size = 1422025, upload-time = "2026-09-27T05:20:07.72Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f4/e36c4a95b7997e213cd85c3be1232c8ac19e1ecf20d99512ea279eade321/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:5714ac1e72ba5faa645f593de353af34a8b1de8bcff811f1d3da4c1b8628e1c3", size = 1374166, upload-time = "2026-09-27T05:21:01.762Z" }, - { url = "https://files.pythonhosted.org/packages/18/9e/b9c3e282e9833e75875bc9163f80c49f8153d0f96ce5ba034a7997f13850/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:dffa4c97f0b473f1da456807ff57b348fb675f7728a15faab4eea87dffb6c77a", size = 1278387, upload-time = "2026-09-27T05:20:05.125Z" }, - { url = "https://files.pythonhosted.org/packages/af/96/84ed26fd0855ad8d6d67413893c835e90a77294c1255e3e78c13fe082f0f/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:29c1d761ef45e063eba2f8287e0572976f6b758b6832d75d7e03d02a610d3a91", size = 1297784, upload-time = "2026-09-27T05:20:28.101Z" }, - { url = "https://files.pythonhosted.org/packages/94/19/c94f1bce7e72950f8f3c5ee9d34ba98a7da70548453a96befa86281bfd94/hypothesis-6.168.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c7ab9ecf409143ef26a0d20a5f373d2add8871be58028cdbd3a515b0ef43ac45", size = 1333959, upload-time = "2026-09-27T05:21:16.771Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/f2e4bedd7031fb46b35ed9f95841074d990b504c969af6ad18f91e989bdf/hypothesis-6.168.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:bc15fff253b43cbbbe45dafa5773fbf122eeeb9449098ec3cfcdaf9e2aae234d", size = 675380, upload-time = "2026-09-27T05:20:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/77/df/c97684c2698ef16032c99506308ee96270770addb640d2b154f1bc77a26f/hypothesis-6.168.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:67d5d6f273e51c9832d637af5d45c8d95cfced1f93760ce2056f9886c61c5f7e", size = 681683, upload-time = "2026-09-27T05:21:59.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/85/e3c41febc99d939660f5c26d160f9992662d7cf99c62e60bd4c2419c36f7/hypothesis-6.168.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:3c91cde9298b2da2a2e508c752b1531ed26ecd66b67b6a1af75d7b870bc92fff", size = 679372, upload-time = "2026-09-27T05:20:24.066Z" }, - { url = "https://files.pythonhosted.org/packages/d9/dc/d765a012e2e31dd38f899487ec28d4bc33eb5cbf16ca4f90023005f5a0de/hypothesis-6.168.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ff8453251976d8090b323eede58dde1b77f99404a3c4c2d0eebf5bfedbd34d94", size = 793121, upload-time = "2026-09-27T05:21:25.179Z" }, - { url = "https://files.pythonhosted.org/packages/72/fa/616344dcf5e4ec9952dde2d8b81ff6dc4652ea3efb5511ba8b4e1a36273f/hypothesis-6.168.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:58e471bb09351a1c12f1a5da053f43d23dc359cddb3fd9c0d56ef6f68649751e", size = 788974, upload-time = "2026-09-27T05:21:15.158Z" }, - { url = "https://files.pythonhosted.org/packages/f9/02/0a5d02cfc75194a410e46763463d8b8cf1f51437633ffcf7f345f556dccc/hypothesis-6.168.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ad8f0e1f1b32f293ead803bc16ce20066414b14f36d594d170119520ccc3e8", size = 1124963, upload-time = "2026-09-27T05:20:26.494Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d5/99044493d2bda5a6213dc0df7ffd52b91447bfa4817031a751299887a408/hypothesis-6.168.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b70add4949a4be468b5461e01b125d42d58ad03f92da79261e9aab25eff9002", size = 1171944, upload-time = "2026-09-27T05:20:48.285Z" }, - { url = "https://files.pythonhosted.org/packages/c4/db/8ef748e3f55bdba4a04b805bdc29c4462b2e5280900f0dc9347a0f0167cb/hypothesis-6.168.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d53ff2916fded1ab8b8abe83c33e8cafb4f9a8cabeca114a037bdb8bd715f2df", size = 685625, upload-time = "2026-09-27T05:21:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/bd/80/4efe245037b2d2ff64e220fecb4656f41a3d5e296a65daa6d6e4245e1400/hypothesis-6.168.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2", size = 792323, upload-time = "2026-09-28T05:19:31.294Z" }, + { url = "https://files.pythonhosted.org/packages/49/cd/0f96010943e93d07fe0c064a6c1812b33d305f29fa43606a8b863ba806af/hypothesis-6.168.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742", size = 788335, upload-time = "2026-09-28T05:20:29.748Z" }, + { url = "https://files.pythonhosted.org/packages/12/a9/95ebd471169fef0ccb7c9194ed643797eab67703b927efb6e76c046a78ce/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05", size = 1124433, upload-time = "2026-09-28T05:19:01.479Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3a/153bf04b7ce7f14d563b943af5f05347d5f91948f66f59655b9913b97531/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74", size = 1170640, upload-time = "2026-09-28T05:18:40.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/3d5ca014d00897dc5f69fa45374d86014fde89938e381303d597275b4ad3/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c", size = 1300447, upload-time = "2026-09-28T05:18:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/18/76/38fb90efa47cab7d93b2794624a8177533bc748ee4666681a0ece1e3c23b/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083", size = 1336918, upload-time = "2026-09-28T05:18:45.505Z" }, + { url = "https://files.pythonhosted.org/packages/78/44/92b10c9df3029379cc67ad2d3af17cd1b35a72516b0ef37d5b55a125caec/hypothesis-6.168.3-cp310-cp310-win_amd64.whl", hash = "sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0", size = 684844, upload-time = "2026-09-28T05:19:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, ] [[package]] From d7d5a1acac36392365d7c88f7d19521610971cef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:06:14 +0200 Subject: [PATCH 138/322] chore: Bump the all-npm group with 2 updates (#5042) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 216 +++++++++++++++++++++++----------------------- package.json | 4 +- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2801097a8..73056adc0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.6.3", + "@types/node": "^26.6.4", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.5", + "rollup": "^4.64.0", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", - "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", + "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", - "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", + "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", - "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", + "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", - "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", + "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", - "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", + "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", - "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", + "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", - "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", + "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", - "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", + "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", - "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", + "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", - "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", + "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", - "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", + "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", - "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", + "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", - "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", + "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", - "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", + "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", - "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", + "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", - "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", + "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", - "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", + "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", - "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", + "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", - "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", + "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", - "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", + "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", - "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", + "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", - "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", + "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", - "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", + "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", - "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", + "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", - "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", + "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.63.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", - "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz", + "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.5", - "@rollup/rollup-android-arm64": "4.63.5", - "@rollup/rollup-darwin-arm64": "4.63.5", - "@rollup/rollup-darwin-x64": "4.63.5", - "@rollup/rollup-freebsd-arm64": "4.63.5", - "@rollup/rollup-freebsd-x64": "4.63.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", - "@rollup/rollup-linux-arm-musleabihf": "4.63.5", - "@rollup/rollup-linux-arm64-gnu": "4.63.5", - "@rollup/rollup-linux-arm64-musl": "4.63.5", - "@rollup/rollup-linux-loong64-gnu": "4.63.5", - "@rollup/rollup-linux-loong64-musl": "4.63.5", - "@rollup/rollup-linux-ppc64-gnu": "4.63.5", - "@rollup/rollup-linux-ppc64-musl": "4.63.5", - "@rollup/rollup-linux-riscv64-gnu": "4.63.5", - "@rollup/rollup-linux-riscv64-musl": "4.63.5", - "@rollup/rollup-linux-s390x-gnu": "4.63.5", - "@rollup/rollup-linux-x64-gnu": "4.63.5", - "@rollup/rollup-linux-x64-musl": "4.63.5", - "@rollup/rollup-openbsd-x64": "4.63.5", - "@rollup/rollup-openharmony-arm64": "4.63.5", - "@rollup/rollup-win32-arm64-msvc": "4.63.5", - "@rollup/rollup-win32-ia32-msvc": "4.63.5", - "@rollup/rollup-win32-x64-gnu": "4.63.5", - "@rollup/rollup-win32-x64-msvc": "4.63.5", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 60e3006d74..b8b6790ce3 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.6.3", + "@types/node": "^26.6.4", "concurrently": "^10.0.5", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.63.5", + "rollup": "^4.64.0", "terser": "^5.51.2", "typescript": "^7.0.2" }, From c2b0a571efaff0ed9ccf8c9f7e5b72c48e02dac7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:07:21 +0200 Subject: [PATCH 139/322] chore: Bump @fluentui/react from 8.125.7 to 8.125.8 in /tests/React in the all-npm group (#5043) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/React/package-lock.json | 121 ++++++++++++++++++---------------- tests/React/package.json | 2 +- 2 files changed, 64 insertions(+), 59 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index 41ac80119c..f9d368cd7b 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.7", + "@fluentui/react": "^8.125.8", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.7", @@ -721,27 +721,27 @@ } }, "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.74", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.74.tgz", - "integrity": "sha512-q3QiQ2CuFdMjtiP9U5+Qi8LMhxKqWEi7C8R+O6SuaGbGS+3YOO/1iuenhKtt961OHn5ev/0M4j4okUZ1112GSw==", + "version": "8.5.75", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.75.tgz", + "integrity": "sha512-BSXGRmANjOpYvDboBf6j50vJCLJvb3d4DoOZS6xkyDMwEXdWO1ExTM+VECtA67FpmdSRRlZFpayTCqQ9hJkL2Q==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.3", + "@fluentui/utilities": "^8.17.3", "tslib": "^2.1.0" } }, "node_modules/@fluentui/foundation-legacy": { - "version": "8.6.7", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.7.tgz", - "integrity": "sha512-mu1wHlkom+XT+mlaGe7GW4eIZeIT9OjgdXg2Az/P670N7JA1OKcjHd8F/9ktMb5FGrXJ287AjQDNCDfWnjnCYA==", + "version": "8.6.8", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.8.tgz", + "integrity": "sha512-Z00gGL7l+l/qwovK7E52cjOAccCpOt8Ziw4JUYhbo+qsAn59auE6xrH+6HwwHibKqKymSO2KsJ4vkpJC4iT0MA==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.15", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.3", + "@fluentui/utilities": "^8.17.3", "tslib": "^2.1.0" }, "peerDependencies": { @@ -759,9 +759,9 @@ } }, "node_modules/@fluentui/merge-styles": { - "version": "8.6.14", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.14.tgz", - "integrity": "sha512-vghuHFAfQgS9WLIIs4kgDOCh/DHd5vGIddP4/bzposhlAVLZR6wUBqldm9AuCdY88r5LyCRMavVJLV+Up3xdvA==", + "version": "8.6.15", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.15.tgz", + "integrity": "sha512-aNob5pyHJ9wxyr/E79+YTrS9B0kkUcaHoY0dfeCVM+VF4IvWOr8u/9Cxan7vpYUMXy93QWPeDxAVmlzsRTlEpA==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", @@ -769,23 +769,23 @@ } }, "node_modules/@fluentui/react": { - "version": "8.125.7", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.7.tgz", - "integrity": "sha512-MsputVjgAZEcSzfr6nXBghfZnqu+GKT4NCBwLeSL8yyQEugUxyZJNp4TVZblGdMognyRDbobNn+/zIhEK4sxNQ==", + "version": "8.125.8", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.8.tgz", + "integrity": "sha512-f/3RvfYCBBqACuPAVb/k3yxIPOHfdfm1AGxdoegRZXZ5cRRW/6WtGZwjb15jc6E/dMEOJjc/8BfLfjYl2A4V7A==", "license": "MIT", "dependencies": { "@fluentui/date-time-utilities": "^8.6.11", - "@fluentui/font-icons-mdl2": "^8.5.74", - "@fluentui/foundation-legacy": "^8.6.7", - "@fluentui/merge-styles": "^8.6.14", - "@fluentui/react-focus": "^8.10.7", - "@fluentui/react-hooks": "^8.10.2", - "@fluentui/react-portal-compat-context": "^9.0.15", + "@fluentui/font-icons-mdl2": "^8.5.75", + "@fluentui/foundation-legacy": "^8.6.8", + "@fluentui/merge-styles": "^8.6.15", + "@fluentui/react-focus": "^8.10.8", + "@fluentui/react-hooks": "^8.10.3", + "@fluentui/react-portal-compat-context": "^9.0.17", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/theme": "^2.7.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.3", + "@fluentui/theme": "^2.7.3", + "@fluentui/utilities": "^8.17.3", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" }, @@ -797,16 +797,16 @@ } }, "node_modules/@fluentui/react-focus": { - "version": "8.10.7", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.7.tgz", - "integrity": "sha512-Uf0wliddeVPEYylNvbBqYu2V2ofCQ/YhC5JHIFT4RwBkZil1Ta3X+9EFhJMO1LejyLXTKnMMimPxdPS1XzLTGQ==", + "version": "8.10.8", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.8.tgz", + "integrity": "sha512-k3845czVJHJsURlLypIEbl6A3gh9PAc+Y/41FvBQEcgaTw2BNS7rNQgq4xks3iKy6JcmpY/7ZZI/4BT/CBB0rg==", "license": "MIT", "dependencies": { "@fluentui/keyboard-key": "^0.4.23", - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.15", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/style-utilities": "^8.15.3", + "@fluentui/utilities": "^8.17.3", "tslib": "^2.1.0" }, "peerDependencies": { @@ -815,14 +815,14 @@ } }, "node_modules/@fluentui/react-hooks": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.2.tgz", - "integrity": "sha512-HAd5cX50yKW/LljWlwt+FpSpdS/pNJutk9kMb7FyzxfoGBulL7sj6vX2HvxhSKyJMRKuTstXTdfJmsh22+3W3w==", + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.3.tgz", + "integrity": "sha512-fFy8Rhs/dpFCJ3X+2jv9pc4KRn6iEOaRqBqaUXKjOvIWyQDe8/LGupRWUYEHdniroZcPWEbgGulFYjLvGP0wzw==", "license": "MIT", "dependencies": { "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.2", + "@fluentui/utilities": "^8.17.3", "tslib": "^2.1.0" }, "peerDependencies": { @@ -831,9 +831,9 @@ } }, "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.15", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.15.tgz", - "integrity": "sha512-DpV+qtFvM3dmH1j8ZD+YcM5vaTvmQPHUAx6tQnnmIoYJWs2R0wU/L5p2EajXy7zSg74jrDbDRxzaziamoOaJdg==", + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.17.tgz", + "integrity": "sha512-zfRwL3EeOXCNELCd4C+Yz2LsdVVnyvh0+pyXLDPDxN+r7Hj434dFdS5D4HWv/k0EYzkfoBbACfilmB6h9kviJg==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" @@ -841,6 +841,11 @@ "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "react": ">=16.14.0 <20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@fluentui/react-window-provider": { @@ -867,28 +872,28 @@ } }, "node_modules/@fluentui/style-utilities": { - "version": "8.15.2", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.2.tgz", - "integrity": "sha512-rss7pkgiyNQuo1OpS7R2X6IJXIwDzQ0fxQxIeBB7Xp7lM+AwcKn5V7fzgIgCyE7BI6TWSy2TeQJ9Jtr/4fYEaw==", + "version": "8.15.3", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.3.tgz", + "integrity": "sha512-zuyR421wNDN9CTykQABWNHm1X/3k+uTRDYQGvrpxLw8PKI9rB8OhmE+oZZLq2rzh1R3ZUiQV3+6LTe9D/8FYFA==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.15", "@fluentui/set-version": "^8.2.24", - "@fluentui/theme": "^2.7.2", - "@fluentui/utilities": "^8.17.2", + "@fluentui/theme": "^2.7.3", + "@fluentui/utilities": "^8.17.3", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "node_modules/@fluentui/theme": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.2.tgz", - "integrity": "sha512-UXGNfGa/1bLmYrOpmHXdvyc7CzlNSKUQAADweTncbNoMF1DvscWEjPj5kxFgCmOU8wVtvvn4GraNNUSWtNxeeA==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.3.tgz", + "integrity": "sha512-DrzVYJS8gIxMsbZ8CI/hFQu4LcDsPyVag5McKCytrrvRLBAskD4bRdkTlyljhK2jUEDCsVMl9KWxnqsqy7SVHw==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.15", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.2", + "@fluentui/utilities": "^8.17.3", "tslib": "^2.1.0" }, "peerDependencies": { @@ -897,13 +902,13 @@ } }, "node_modules/@fluentui/utilities": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.2.tgz", - "integrity": "sha512-TmeWVtGN+Lk0mch7tuRcbkeMdrBwltI68fvQbPwcNLo4igFtTInMmjEnVJGa7pBQN5lQAmHYqB9IJI6RZU/t6w==", + "version": "8.17.3", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.3.tgz", + "integrity": "sha512-lMk+/k7kVGXSlLC86Z0MlMrL5aeaLuDbF2GG25hiWqMBqh2dJVeqwgqruZDt5rYXs0SEBAls3YvvN/IR81PQyw==", "license": "MIT", "dependencies": { "@fluentui/dom-utilities": "^2.3.10", - "@fluentui/merge-styles": "^8.6.14", + "@fluentui/merge-styles": "^8.6.15", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", "tslib": "^2.1.0" @@ -1837,9 +1842,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" diff --git a/tests/React/package.json b/tests/React/package.json index 309ab4c568..57fe06b0dd 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -9,7 +9,7 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.7", + "@fluentui/react": "^8.125.8", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.7", From 23bc5c54db51469431e6fdab713dfe1b6a3df019 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:27:02 +0200 Subject: [PATCH 140/322] chore: Bump the all-nuget group with 1 update (#5045) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 58ff7c3df9..a6f5d34e4c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.5", + "version": "8.0.7", "commands": [ "fantomas" ], From c2127aabd6dd72583ce20d3a7e7f5bc5f646186a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:42:11 +0200 Subject: [PATCH 141/322] chore: release multiple projects (#5034) --- src/Fable.Cli/CHANGELOG.md | 16 +++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 16 +++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 15 ++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-standalone/CHANGELOG.md | 15 ++++++++++++++- src/fable-standalone/package.json | 4 ++-- 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 177b0b9b7e..decd31de69 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 +last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.20.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) +* *(python)* Share per-object locks and align StartChild timeouts (#5035) ([2da7144c](https://github.com/fable-compiler/Fable/commit/2da7144c68efded840fc6585c23d3a4e28e8e37f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + ## 5.19.0 - 2026-10-03 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index c3730b1d07..e9e0f73271 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 +last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.19.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) +* *(python)* Share per-object locks and align StartChild timeouts (#5035) ([2da7144c](https://github.com/fable-compiler/Fable/commit/2da7144c68efded840fc6585c23d3a4e28e8e37f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + ## 5.18.0 - 2026-10-03 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 83e471390a..ee3e965925 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.19.0" + let VERSION = "5.20.0" [] let JS_LIBRARY_VERSION = "2.8.0" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 9180da009d..53187bcdee 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 +last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.7.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + ## 3.6.0 - 2026-10-03 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index b1b9be26d8..46c0be42a0 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.6.0", + "version": "3.7.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.6.0" + "@fable-org/fable-standalone": "^3.7.0" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index fe7316460f..0ada496219 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: aaf35f200be5bbc2698abe242f77b8de745e4337 +last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.7.0 - 2026-10-07 + +### 🚀 Features + +* *(python)* Give System exceptions native Python bases (#5040) ([4ee31586](https://github.com/fable-compiler/Fable/commit/4ee31586fd88ed0708e218e7f170432baa33551e)) + +### 🐞 Bug Fixes + +* *(beam)* Recognize boxed discriminated unions in type tests (#5033) ([dcfaa3d6](https://github.com/fable-compiler/Fable/commit/dcfaa3d6181f6789d0868e0bf9132ecdcdc76287)) +* *(beam)* Prevent identifier collisions and atom overflow (#5017) ([b9ce2fdb](https://github.com/fable-compiler/Fable/commit/b9ce2fdb53fc86a615970f4828005ee3a490385f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/aaf35f200be5bbc2698abe242f77b8de745e4337..23bc5c54db51469431e6fdab713dfe1b6a3df019) + ## 3.6.0 - 2026-10-03 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index d9b708520b..787033ae5e 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.6.0", - "fableVersion": "5.19.0", + "version": "3.7.0", + "fableVersion": "5.20.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From b6184ceb8284b1dd2257dfeb609dd3b6c5302a22 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 7 Oct 2026 22:23:38 +0200 Subject: [PATCH 142/322] chore: wait for fable-standalone on npm before building compiler-js --- src/Fable.Build/AutomatedRelease.fs | 5 +---- src/Fable.Build/Publish.fs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Fable.Build/AutomatedRelease.fs b/src/Fable.Build/AutomatedRelease.fs index a49637e4e8..e1a6afb371 100644 --- a/src/Fable.Build/AutomatedRelease.fs +++ b/src/Fable.Build/AutomatedRelease.fs @@ -52,10 +52,7 @@ let handle (args: string list) = // to the new version before that version is on npm. Standalone.handle [] Publish.publishNpm ProjectDir.fable_standalone - - // Give npm some time to make the new fable-standalone version available - // before CompilerJs.handle tries to install it. - System.Threading.Thread.Sleep(System.TimeSpan.FromSeconds(15.)) + Publish.waitForNpmAvailability ProjectDir.fable_standalone CompilerJs.handle [ "--skip-fable-standalone" ] diff --git a/src/Fable.Build/Publish.fs b/src/Fable.Build/Publish.fs index c1a81ec7f3..8ec96b561e 100644 --- a/src/Fable.Build/Publish.fs +++ b/src/Fable.Build/Publish.fs @@ -85,6 +85,24 @@ let publishNpm (projectDir: string) = else printfn $"Already up-to-date, skipping..." +let waitForNpmAvailability (projectDir: string) = + let packageJsonFile = Path.Combine(projectDir, "package.json") |> FileInfo + + let packageSpec = + PackageJson.getName packageJsonFile + + "@" + + PackageJson.getVersion packageJsonFile + + let timeout = TimeSpan.FromMinutes 10. + let stopwatch = Diagnostics.Stopwatch.StartNew() + + while PackageJson.needPublishing packageJsonFile do + if stopwatch.Elapsed > timeout then + failwith $"%s{packageSpec} is still not available on npm after %.0f{timeout.TotalMinutes} minutes" + + printfn $"Waiting for %s{packageSpec} to be available on npm..." + Threading.Thread.Sleep(TimeSpan.FromSeconds 10.) + let handle (args: string list) = // Build all the fable-libraries // Force rebuld of fable-libraries to make sure they are generated with the latest @@ -136,6 +154,7 @@ let handle (args: string list) = // to the new version before that version is on npm. Standalone.handle [] publishNpm ProjectDir.fable_standalone + waitForNpmAvailability ProjectDir.fable_standalone CompilerJs.handle [ "--skip-fable-standalone" ] From b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 8 Oct 2026 00:43:02 +0200 Subject: [PATCH 143/322] fix(beam): disambiguate property accessors from ordinary methods (#5049) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 12 +++++- src/Fable.Transforms/Beam/Fable2Beam.fs | 6 +-- src/Fable.Transforms/Beam/Prelude.fs | 3 ++ src/Fable.Transforms/Beam/Replacements.fs | 7 ++++ src/Fable.Transforms/FSharp2Fable.Util.fs | 5 +++ src/Fable.Transforms/Global/Naming.fs | 2 + tests/Beam/Fable.Tests.Beam.fsproj | 2 + .../MemberNameCollisionLibrary/Library.fs | 26 ++++++++++++ .../MemberNameCollisionLibrary.fsproj | 9 ++++ tests/Beam/MemberNamingTests.fs | 42 +++++++++++++++++++ 10 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 tests/Beam/MemberNameCollisionLibrary/Library.fs create mode 100644 tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj create mode 100644 tests/Beam/MemberNamingTests.fs diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 7602a28c66..6b3e7896e8 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -118,6 +118,16 @@ words receive a trailing underscore. Encoded punctuation in F# compiled names us remaining module-level name/arity collisions fail compilation. Reflection metadata stores both the original F# name and the emitted atom when both are needed. +Property accessors carry an `_accessor` suffix after any overload hash. This keeps +`set_StatusCode` distinct from the ordinary method `SetStatusCode`, and likewise +`get_StatusCode` from `GetStatusCode`, despite snake_case sanitization. Declarations +and calls, including calls from other files and referenced projects or Fable +packages, derive the suffix from the same F# member metadata. Ordinary method names +are unchanged; genuine remaining name/arity collisions still fail compilation. +This changes generated accessor names: rebuild generated libraries, the BEAM +runtime, and their callers together, and update native Erlang calls to those +accessors. The public F# property and method APIs are unchanged. + Names are capped at 223 characters to leave room for atoms generated by `erlc`. Native compilation uses a SHA-256 suffix for overlong names. Standalone compilation uses the existing 32-bit djb2 hash to avoid cryptography dependencies in the web UI; @@ -486,7 +496,7 @@ suite grows. | Options | Erasure can still conflate `None`, `Some null`/`Some undefined`, and some nested option paths after static type information is lost. | | `char` | A generic or `obj`-erased character is an integer at runtime, so `string` and `%A` can print its codepoint. UTF-16 surrogate behavior is not complete. | | Structured formatting | `%A` reconstructs values from term shape. Record field order, original names, erased options, sets, chars, refs/arrays, decimals, and date/time values can differ from .NET output. | -| Identifiers | Instance members split across declaration paths can still sanitize to the same Erlang function name and arity. | +| Identifiers | Property accessors and ordinary `Get`/`Set` methods have distinct generated names. Other distinctions lost by sanitization, such as `work` versus `Work`, can still collide; detected name/arity collisions fail compilation. | | Type tests | Erlang cannot distinguish integer widths or unrelated F# types with the same runtime shape. Some interface/class downcasts and abstract/base dispatch paths are unsupported. | | Classes and structs | Mutable record fields, class reference equality, some self-referencing/base constructors, mutually recursive class hierarchies, and default struct construction remain incomplete. | | Module initialization | Module-level mutable values and snapshots exist only in a process that ran the generated module `main/0`; ordinary library calls and other processes can read `undefined`. | diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 87d48dd16f..058dc29f7b 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -3489,10 +3489,8 @@ and transformClassDeclaration ) | _ -> () - // Deduplicate functions by name+arity. This can happen when a property setter - // (e.g., `set StatusCode`) and a method (e.g., `SetStatusCode`) both mangle to - // the same Erlang function name. Unlike JS/Python which have native getter/setter - // syntax, Erlang uses plain functions so name collisions produce duplicate definitions. + // Report remaining name/arity collisions, retaining one recovery form so error output + // stays printable. Accessor/method pairs already have distinct upstream names. let allForms = constructorForms @ memberForms @ reflectionForms let reflectionKeys = diff --git a/src/Fable.Transforms/Beam/Prelude.fs b/src/Fable.Transforms/Beam/Prelude.fs index 994403ad9b..5fd2be6b16 100644 --- a/src/Fable.Transforms/Beam/Prelude.fs +++ b/src/Fable.Transforms/Beam/Prelude.fs @@ -548,6 +548,9 @@ module Naming = let classFieldAtomName (name: string) = limitErlangAtomLength ("field_" + sanitizeErlangName name) + /// Mark a generated accessor before atom sanitization, including runtime replacement calls. + let accessorFunctionName name = name + Fable.Naming.beamAccessorSuffix + /// Derive the map key shared by interface implementations, dispatch, and collision diagnostics. let interfaceMemberKey isSetter (name: string) = let memberName = sanitizeErlangName name diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 6890cdf1e9..92872cb0d3 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5596,6 +5596,13 @@ let private getMangledNames (i: CallInfo) (thisArg: Expr option) = let mangledName = Naming.buildNameWithoutSanitationFrom entityName isStatic memberName i.OverloadSuffix + let mangledName = + match i.CompiledName with + // decision: Match the runtime property explicitly; get_Chars is an ordinary F# method despite its BCL accessor spelling. + // invariant: StringBuilder.Length calls use the same accessor marker as its F# runtime declaration. + | "get_Length" -> accessorFunctionName mangledName + | _ -> mangledName + moduleName, mangledName let private bclType (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 13d68c3871..cd84edd7dd 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -793,6 +793,11 @@ module Helpers = |> Fable.Py.Naming.toPythonNaming | Rust -> Naming.buildNameWithoutSanitation name part | Dart -> Naming.sanitizeDartIdent (fun _ -> false) name part + | Beam when memb.IsProperty || memb.IsPropertyGetterMethod || memb.IsPropertySetterMethod -> + // decision: BEAM accessors carry a kind marker because snake_case merges `set_Foo` with `SetFoo` (and `get_Foo` with `GetFoo`). + // invariant: A CLI event's property symbol and its getter method receive the same marker. + // invariant: Declarations and local, cross-file, and referenced-library calls derive the accessor marker from the same member metadata. + Naming.sanitizeJsIdent (fun _ -> false) name part + Naming.beamAccessorSuffix | _ -> Naming.sanitizeJsIdent (fun _ -> false) name part let hasOverloadSuffix = not (String.IsNullOrEmpty(part.OverloadSuffix)) diff --git a/src/Fable.Transforms/Global/Naming.fs b/src/Fable.Transforms/Global/Naming.fs index 3ebd9a41e5..abec6b2d0c 100644 --- a/src/Fable.Transforms/Global/Naming.fs +++ b/src/Fable.Transforms/Global/Naming.fs @@ -449,6 +449,8 @@ module Naming = let reflectionSuffix = "_$reflection" + let beamAccessorSuffix = "_accessor" + let private printPart sanitize separator part overloadSuffix = (if String.IsNullOrEmpty(part) then "" diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 9cdcbf32ed..54bd8eb912 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -22,6 +22,7 @@ + @@ -102,6 +103,7 @@ + diff --git a/tests/Beam/MemberNameCollisionLibrary/Library.fs b/tests/Beam/MemberNameCollisionLibrary/Library.fs new file mode 100644 index 0000000000..36fcc7b668 --- /dev/null +++ b/tests/Beam/MemberNameCollisionLibrary/Library.fs @@ -0,0 +1,26 @@ +module Fable.Tests.MemberNameCollisionLibrary + +type Response() = + let mutable status = 0 + + member _.StatusCode + with get () = status + and set (value: int) = status <- value + + member _.SetStatusCode(value: int) = status <- value + 1 + member _.GetStatusCode() = status + 2 + +let propertyPath () = + let response = Response() + response.StatusCode <- 201 + response.StatusCode + +let methodPath () = + let response = Response() + response.SetStatusCode 201 + response.StatusCode + +let getterMethodPath () = + let response = Response() + response.StatusCode <- 201 + response.GetStatusCode() diff --git a/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj b/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj new file mode 100644 index 0000000000..df9b203b99 --- /dev/null +++ b/tests/Beam/MemberNameCollisionLibrary/MemberNameCollisionLibrary.fsproj @@ -0,0 +1,9 @@ + + + net10.0 + false + + + + + diff --git a/tests/Beam/MemberNamingTests.fs b/tests/Beam/MemberNamingTests.fs new file mode 100644 index 0000000000..2c193a4e5c --- /dev/null +++ b/tests/Beam/MemberNamingTests.fs @@ -0,0 +1,42 @@ +module Fable.Tests.MemberNamingTests + +open Fable.Tests.Util +open Util.Testing +open Fable.Tests.MemberNameCollisionLibrary + +[] +let ``test Same-file property setter keeps its own body`` () = + propertyPath () |> equal 201 + +[] +let ``test Same-file ordinary setter method keeps its own body`` () = + methodPath () |> equal 202 + +[] +let ``test Same-file ordinary getter method keeps its own body`` () = + getterMethodPath () |> equal 203 + +[] +let ``test Referenced-library property setter resolves across files`` () = + let response = Response() + response.StatusCode <- 201 + response.StatusCode |> equal 201 + +[] +let ``test Referenced-library ordinary setter method resolves across files`` () = + let response = Response() + response.SetStatusCode 201 + response.StatusCode |> equal 202 + +[] +let ``test Referenced-library ordinary getter method resolves across files`` () = + let response = Response() + response.StatusCode <- 201 + response.GetStatusCode() |> equal 203 + +[] +let ``test Referenced-library method reference keeps the ordinary method body`` () = + let response = Response() + let setStatus = response.SetStatusCode + setStatus 201 + response.StatusCode |> equal 202 From 4e486f201fb229964af213d6ce3030ac3c7e26de Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:13:51 +0800 Subject: [PATCH 144/322] feat(all): add implicit conversion to erased unions U2-U9 Co-authored-by: Mangel Maxime --- src/Fable.Core/Fable.Core.Types.fs | 118 ++++++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 9 deletions(-) diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index 1e28ded11f..6d6e2ca30a 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -227,8 +227,15 @@ type NamedParamsAttribute = ParamObjectAttribute type InjectAttribute() = inherit Attribute() +/// /// Erased union type to represent one of two possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U2<'a, 'b> = | Case1 of 'a @@ -236,9 +243,18 @@ type U2<'a, 'b> = static member op_ErasedCast(x: 'a) = Case1 x static member op_ErasedCast(x: 'b) = Case2 x + static member inline op_Implicit(x: 'a) : U2<'a, 'b> = Case1 x + static member inline op_Implicit(x: 'b) : U2<'a, 'b> = Case2 x +/// /// Erased union type to represent one of three possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U3<'a, 'b, 'c> = | Case1 of 'a @@ -248,9 +264,19 @@ type U3<'a, 'b, 'c> = static member op_ErasedCast(x: 'a) = Case1 x static member op_ErasedCast(x: 'b) = Case2 x static member op_ErasedCast(x: 'c) = Case3 x + static member inline op_Implicit(x: 'a) : U3<'a, 'b, 'c> = Case1 x + static member inline op_Implicit(x: 'b) : U3<'a, 'b, 'c> = Case2 x + static member inline op_Implicit(x: 'c) : U3<'a, 'b, 'c> = Case3 x +/// /// Erased union type to represent one of four possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U4<'a, 'b, 'c, 'd> = | Case1 of 'a @@ -262,9 +288,20 @@ type U4<'a, 'b, 'c, 'd> = static member op_ErasedCast(x: 'b) = Case2 x static member op_ErasedCast(x: 'c) = Case3 x static member op_ErasedCast(x: 'd) = Case4 x + static member inline op_Implicit(x: 'a) : U4<'a, 'b, 'c, 'd> = Case1 x + static member inline op_Implicit(x: 'b) : U4<'a, 'b, 'c, 'd> = Case2 x + static member inline op_Implicit(x: 'c) : U4<'a, 'b, 'c, 'd> = Case3 x + static member inline op_Implicit(x: 'd) : U4<'a, 'b, 'c, 'd> = Case4 x +/// /// Erased union type to represent one of five possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U5<'a, 'b, 'c, 'd, 'e> = | Case1 of 'a @@ -278,9 +315,21 @@ type U5<'a, 'b, 'c, 'd, 'e> = static member op_ErasedCast(x: 'c) = Case3 x static member op_ErasedCast(x: 'd) = Case4 x static member op_ErasedCast(x: 'e) = Case5 x + static member inline op_Implicit(x: 'a) : U5<'a, 'b, 'c, 'd, 'e> = Case1 x + static member inline op_Implicit(x: 'b) : U5<'a, 'b, 'c, 'd, 'e> = Case2 x + static member inline op_Implicit(x: 'c) : U5<'a, 'b, 'c, 'd, 'e> = Case3 x + static member inline op_Implicit(x: 'd) : U5<'a, 'b, 'c, 'd, 'e> = Case4 x + static member inline op_Implicit(x: 'e) : U5<'a, 'b, 'c, 'd, 'e> = Case5 x +/// /// Erased union type to represent one of six possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U6<'a, 'b, 'c, 'd, 'e, 'f> = | Case1 of 'a @@ -296,9 +345,22 @@ type U6<'a, 'b, 'c, 'd, 'e, 'f> = static member op_ErasedCast(x: 'd) = Case4 x static member op_ErasedCast(x: 'e) = Case5 x static member op_ErasedCast(x: 'f) = Case6 x + static member inline op_Implicit(x: 'a) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case1 x + static member inline op_Implicit(x: 'b) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case2 x + static member inline op_Implicit(x: 'c) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case3 x + static member inline op_Implicit(x: 'd) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case4 x + static member inline op_Implicit(x: 'e) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case5 x + static member inline op_Implicit(x: 'f) : U6<'a, 'b, 'c, 'd, 'e, 'f> = Case6 x +/// /// Erased union type to represent one of seven possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = | Case1 of 'a @@ -316,9 +378,23 @@ type U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = static member op_ErasedCast(x: 'e) = Case5 x static member op_ErasedCast(x: 'f) = Case6 x static member op_ErasedCast(x: 'g) = Case7 x + static member inline op_Implicit(x: 'a) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case1 x + static member inline op_Implicit(x: 'b) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case2 x + static member inline op_Implicit(x: 'c) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case3 x + static member inline op_Implicit(x: 'd) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case4 x + static member inline op_Implicit(x: 'e) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case5 x + static member inline op_Implicit(x: 'f) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case6 x + static member inline op_Implicit(x: 'g) : U7<'a, 'b, 'c, 'd, 'e, 'f, 'g> = Case7 x +/// /// Erased union type to represent one of eight possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = | Case1 of 'a @@ -338,9 +414,24 @@ type U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = static member op_ErasedCast(x: 'f) = Case6 x static member op_ErasedCast(x: 'g) = Case7 x static member op_ErasedCast(x: 'h) = Case8 x + static member inline op_Implicit(x: 'a) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case1 x + static member inline op_Implicit(x: 'b) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case2 x + static member inline op_Implicit(x: 'c) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case3 x + static member inline op_Implicit(x: 'd) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case4 x + static member inline op_Implicit(x: 'e) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case5 x + static member inline op_Implicit(x: 'f) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case6 x + static member inline op_Implicit(x: 'g) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case7 x + static member inline op_Implicit(x: 'h) : U8<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h> = Case8 x -/// Erased union type to represent one of nine or more possible values. -/// More info: https://fable.io/docs/communicate/js-from-fable.html#erase-attribute +/// +/// Erased union type to represent one of nine possible values. +/// +/// +/// A value of any case type converts implicitly to the union. +/// +/// Use !^ to convert explicitly, or when the implicit conversion does not apply. +/// +/// [] type U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = | Case1 of 'a @@ -362,6 +453,15 @@ type U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = static member op_ErasedCast(x: 'g) = Case7 x static member op_ErasedCast(x: 'h) = Case8 x static member op_ErasedCast(x: 'i) = Case9 x + static member inline op_Implicit(x: 'a) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case1 x + static member inline op_Implicit(x: 'b) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case2 x + static member inline op_Implicit(x: 'c) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case3 x + static member inline op_Implicit(x: 'd) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case4 x + static member inline op_Implicit(x: 'e) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case5 x + static member inline op_Implicit(x: 'f) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case6 x + static member inline op_Implicit(x: 'g) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case7 x + static member inline op_Implicit(x: 'h) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case8 x + static member inline op_Implicit(x: 'i) : U9<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i> = Case9 x static member inline op_ErasedCast(x: 't) : U9<_, _, _, _, _, _, _, _, ^U> = Case9(^U: (static member op_ErasedCast: 't -> ^U) x) From 4eb2177263f544ec354076da07d8a097d56c273e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:16:39 -0700 Subject: [PATCH 145/322] fix(rust): Fix tailcall elimination for members (#5051) --- src/Fable.Transforms/FableTransforms.fs | 60 ++++++++++++++++++++---- src/Fable.Transforms/FableTransforms.fsi | 5 +- src/Fable.Transforms/Rust/Fable2Rust.fs | 60 +++++++++++------------- src/fable-library-rust/Cargo.toml | 2 +- src/quicktest-rust/Cargo.toml | 4 ++ 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index cdd4b4f196..4742173eb6 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -44,16 +44,56 @@ let getCapturedNames expr = let isIdentCaptured identName expr = walkCapturedIdents (fun candidate -> candidate = identName) expr -let isTailRecursive identName expr = +let isUnitArg (ident: Ident) = + ident.IsCompilerGenerated + && ident.Type = Unit + && (ident.DisplayName.StartsWith("unitVar", System.StringComparison.Ordinal) + || ident.DisplayName.Contains("@")) + +let discardUnitArg (genArgs: Type list) (args: Ident list) = + match genArgs, args with + | [ Unit ], [ arg ] -> args // don't drop unit arg when generic arg is unit + | _ -> + match args with + | [] -> [] + | [ arg ] when isUnitArg arg -> [] + | [ thisArg; arg ] when thisArg.IsThisArgument && isUnitArg arg -> [ thisArg ] + | args -> args + +let discardThisArg (args: Ident list) = + match args with + | thisArg :: args when thisArg.IsThisArgument -> args + | _ -> args + +let isTailRecursive (com: Compiler) name (args: Ident list) body = let mutable isTailRec = true let mutable isRecursive = false - let rec loop inTailPos = - function - | CurriedApply(IdentExpr i, _, _, _) - | Call(IdentExpr i, _, _, _) as e when i.Name = identName -> - isRecursive <- true - isTailRec <- isTailRec && inTailPos + let args = args |> discardUnitArg [] |> discardThisArg + let argTypes = args |> List.map (fun arg -> arg.Type) + + let rec loop inTailPos e = + match e with + | CurriedApply(IdentExpr id, callArgs, _, _) -> + let callArgTypes = callArgs |> List.map (fun arg -> arg.Type) + + if name = id.Name && argTypes = callArgTypes then + isRecursive <- true + isTailRec <- isTailRec && inTailPos + + getSubExpressions e |> List.iter (loop false) + | Call(IdentExpr id, info, _, _) -> + let callArgTypes = info.Args |> List.map (fun arg -> arg.Type) + + let isNameEqual = + match Option.bind com.TryGetMember info.MemberRef with + | Some memb -> name = memb.FullName + | None -> name = id.Name + + if isNameEqual && argTypes = callArgTypes then + isRecursive <- true + isTailRec <- isTailRec && inTailPos + getSubExpressions e |> List.iter (loop false) | Sequential exprs -> let lastIndex = (List.length exprs) - 1 @@ -73,7 +113,7 @@ let isTailRecursive identName expr = List.map snd targets |> List.iter (loop inTailPos) | e -> getSubExpressions e |> List.iter (loop false) - loop true expr + loop true body isTailRec <- isTailRec && isRecursive isRecursive, isTailRec @@ -768,8 +808,8 @@ module private Transforms = Delegate(args, body, name, tags) // Uncurry getters for Rust - | Call(Get(_callee, FieldGet _, _, _), m, _, r) when com.Options.Language = Rust -> - match Option.bind com.TryGetMember m.MemberRef with + | Call(Get(_callee, FieldGet _, _, _), info, _, r) when com.Options.Language = Rust -> + match Option.bind com.TryGetMember info.MemberRef with | Some memb when isGetterOrValueWithoutGenerics memb -> match memb.ReturnParameter.Type with // It may happen the arity of the abstract signature is smaller than actual arity diff --git a/src/Fable.Transforms/FableTransforms.fsi b/src/Fable.Transforms/FableTransforms.fsi index cc54d8948b..714c978340 100644 --- a/src/Fable.Transforms/FableTransforms.fsi +++ b/src/Fable.Transforms/FableTransforms.fsi @@ -4,8 +4,11 @@ open Fable open Fable.AST.Fable val isIdentCaptured: identName: string -> expr: Expr -> bool +val isUnitArg: ident: Ident -> bool +val discardUnitArg: genArgs: Type list -> args: Ident list -> Ident list +val discardThisArg: args: Ident list -> Ident list val getCapturedNames: expr: Expr -> string list -val isTailRecursive: identName: string -> expr: Expr -> bool * bool +val isTailRecursive: com: Compiler -> name: string -> args: Ident list -> body: Expr -> bool * bool val countReferencesUntil: limit: int -> identName: string -> body: Expr -> int val replaceValues: replacements: Map -> expr: Expr -> Expr val uncurryType: typ: Type -> Type diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 2b557b0bb3..229b582dcf 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1381,22 +1381,6 @@ module Util = | Replacements.Util.IsEntity (Types.ienumerable) _ -> ValueSome(Fable.Any) | _ -> ValueNone - let isUnitArg (ident: Fable.Ident) = - ident.IsCompilerGenerated - && ident.Type = Fable.Unit - && (ident.DisplayName.StartsWith("unitVar", StringComparison.Ordinal) - || ident.DisplayName.Contains("@")) - - let discardUnitArg (genArgs: Fable.Type list) (args: Fable.Ident list) = - match genArgs, args with - | [ Fable.Unit ], [ arg ] -> args // don't drop unit arg when generic arg is unit - | _ -> - match args with - | [] -> [] - | [ arg ] when isUnitArg arg -> [] - | [ thisArg; arg ] when thisArg.IsThisArgument && isUnitArg arg -> [ thisArg ] - | args -> args - /// Fable doesn't currently sanitize attached members/fields so we do a simple sanitation here. /// Should this be done in FSharp2Fable step? let sanitizeMember (name: string) = @@ -1427,9 +1411,11 @@ module Util = ctx.UsedNames.CurrentDeclarationScope.Add(name) |> ignore name - type NamedTailCallOpportunity(_com: IRustCompiler, ctx, name, args: Fable.Ident list) = + type NamedTailCallOpportunity(com: IRustCompiler, ctx, name, args: Fable.Ident list) = let args = - args |> discardUnitArg [] |> List.filter (fun arg -> not (arg.IsThisArgument)) + args |> FableTransforms.discardUnitArg [] |> FableTransforms.discardThisArg + + let argTypes = args |> List.map (fun arg -> arg.Type) let label = Fable.Naming.splitLast name @@ -1439,7 +1425,15 @@ module Util = member _.IsRecursiveRef(e) = match e with - | Fable.IdentExpr ident -> name = ident.Name + | Fable.CurriedApply(Fable.IdentExpr id, callArgs, _, _) -> + let callArgTypes = callArgs |> List.map (fun arg -> arg.Type) + name = id.Name && argTypes = callArgTypes + | Fable.Call(Fable.IdentExpr id, info, _, _) -> + let callArgTypes = info.Args |> List.map (fun arg -> arg.Type) + + match Option.bind com.TryGetMember info.MemberRef with + | Some memb -> name = memb.FullName && argTypes = callArgTypes + | None -> name = id.Name && argTypes = callArgTypes | _ -> false let getDecisionTarget (ctx: Context) targetIndex = @@ -3037,9 +3031,10 @@ module Util = mkCallExpr callee args | _ -> + let fableExpr = Fable.Call(calleeExpr, callInfo, typ, range) + match ctx.TailCallOpportunity with - | Some tc when tc.IsRecursiveRef(calleeExpr) && List.length tc.Args = List.length callInfo.Args -> - optimizeTailCall com ctx range tc callInfo.Args + | Some tc when tc.IsRecursiveRef(fableExpr) -> optimizeTailCall com ctx range tc callInfo.Args | _ -> match callInfo.ThisArg, calleeExpr, membOpt with | Some thisArg, Fable.IdentExpr ident, Some memb when memb.IsExtension -> @@ -3260,7 +3255,7 @@ module Util = let prepareLambdaArgsAndCtx ctx args body = let ctx = { ctx with IsLambda = true } let genArgs, ctx = getNewGenArgsAndCtx ctx args body - let args = args |> discardUnitArg genArgs + let args = args |> FableTransforms.discardUnitArg genArgs genArgs, ctx, args let getLetCapturedNames bindings letBody = @@ -3504,7 +3499,7 @@ module Util = mkForLoopExpr None varPat rangeExpr bodyExpr // ?loc=range) let makeLocalLambda com ctx (args: Fable.Ident list) (body: Fable.Expr) = - let args = args |> discardUnitArg [] + let args = args |> FableTransforms.discardUnitArg [] let fnDecl = transformFunctionDecl com ctx args [] Fable.Unit let fnBody = transformExpr com ctx body mkClosureExpr false fnDecl fnBody @@ -3565,9 +3560,10 @@ module Util = | _ -> false let transformCurriedApply (com: IRustCompiler) ctx r typ calleeExpr args = + let fableExpr = Fable.CurriedApply(calleeExpr, args, typ, r) + match ctx.TailCallOpportunity with - | Some tc when tc.IsRecursiveRef(calleeExpr) && List.length tc.Args = List.length args -> - optimizeTailCall com ctx r tc args + | Some tc when tc.IsRecursiveRef(fableExpr) -> optimizeTailCall com ctx r tc args | _ -> let callee = transformCallee com ctx calleeExpr @@ -4472,9 +4468,9 @@ module Util = TailCallOpportunity = tco } - let isTailRecursive (nameOpt: string option) (body: Fable.Expr) = + let isTailRecursive com (nameOpt: string option) (args: Fable.Ident list) (body: Fable.Expr) = match nameOpt with - | Some name -> FableTransforms.isTailRecursive name body + | Some name -> FableTransforms.isTailRecursive com name args body | None -> false, false let transformFunctionBody com ctx (args: Fable.Ident list) (body: Fable.Expr) = @@ -4504,9 +4500,9 @@ module Util = | _ -> transformLeaveContext com ctx None body let transformFunc com ctx parameters returnType (name: string option) (args: Fable.Ident list) (body: Fable.Expr) = - let isRecursive, isTailRec = isTailRecursive name body + let isRecursive, isTailRec = isTailRecursive com name args body let genArgs, ctx = getNewGenArgsAndCtx ctx args body - let args = args |> discardUnitArg genArgs + let args = args |> FableTransforms.discardUnitArg genArgs // let returnType = // match body.Type with @@ -4529,7 +4525,7 @@ module Util = (capturedIdentsOpt: Map option) = let genArgs, ctx, args = prepareLambdaArgsAndCtx ctx args body - let isRecursive, isTailRec = isTailRecursive name body + let isRecursive, isTailRec = isTailRecursive com name args body let fnDecl = transformFunctionDecl com ctx args [] Fable.Unit let ctx = getFunctionBodyCtx com ctx name args body isTailRec @@ -4974,7 +4970,7 @@ module Util = = let ctx = { ctx with IsAssocMember = true } let name = memb.CompiledName - let args = args |> discardUnitArg [] + let args = args |> FableTransforms.discardUnitArg [] let parameters = memb.CurriedParameterGroups |> List.concat let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType let fnDecl = transformFunctionDecl com ctx args parameters returnType @@ -5045,7 +5041,7 @@ module Util = let parameters = memb.CurriedParameterGroups |> List.concat // let returnType = memb.ReturnParameter.Type |> FableTransforms.uncurryType let returnType = body.Type // |> FableTransforms.uncurryType - transformFunc com ctx parameters returnType (Some name) args body + transformFunc com ctx parameters returnType (Some memb.FullName) args body let fnBody = if isIdentAtTailPos (fun ident -> ident.IsThisArgument) body then diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index f6873211cc..e36eed581c 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -31,7 +31,7 @@ num-bigint = { version = "0.5", optional = true } num-integer = { version = "0.1", optional = true } num-traits = { version = "0.2", optional = true } rand = { version = "0.10", features = ["std_rng"], default-features = false, optional = true } -regex = { version = "1.12", optional = true } +regex = { version = "1.13", optional = true } rust_decimal = { version = "1.42", features = ["maths"], default-features = false, optional = true } startup = { version = "0.1", path = "vendored/startup", optional = true } unicode-general-category = { version = "1.1", optional = true } diff --git a/src/quicktest-rust/Cargo.toml b/src/quicktest-rust/Cargo.toml index 54606a6189..792c20cd9c 100644 --- a/src/quicktest-rust/Cargo.toml +++ b/src/quicktest-rust/Cargo.toml @@ -3,6 +3,10 @@ name = "fable_quicktest_rust" version = "0.1.0" edition = "2024" +[[bin]] +name = "fable_quicktest_rust" +path = "src/main.rs" + [features] lrc_ptr = ["fable_library_rust/lrc_ptr"] no_std = ["fable_library_rust/no_std"] From 17b3658876a3bd776c4a0129ec3a4cddc9d7e05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:26:38 -0700 Subject: [PATCH 146/322] fix(rust): Fix temporary struct locals type (#5052) --- src/Fable.Build/Quicktest/Rust.fs | 7 ++++- src/Fable.Transforms/Rust/Fable2Rust.fs | 34 +++++++++++++++++-------- tests/Rust/tests/src/RecordTests.fs | 12 +++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Fable.Build/Quicktest/Rust.fs b/src/Fable.Build/Quicktest/Rust.fs index 91fe780de6..d08715b24a 100644 --- a/src/Fable.Build/Quicktest/Rust.fs +++ b/src/Fable.Build/Quicktest/Rust.fs @@ -2,13 +2,18 @@ module Build.Quicktest.Rust open Build.FableLibrary open Build.Quicktest.Core +open SimpleExec let handle (args: string list) = + let projectDir = "src/quicktest-rust" + + Command.Run("cargo", "clean", workingDirectory = projectDir) + genericQuicktest { Language = "rust" FableLibBuilder = BuildFableLibraryRust() - ProjectDir = "src/quicktest-rust" + ProjectDir = projectDir Extension = ".rs" RunMode = RunScript } diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 229b582dcf..a86bfda20d 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -1469,6 +1469,14 @@ module Util = // transformIdent com ctx None id // | _ -> com.TransformExpr(ctx, expr) + let isTempLocal (ident: Fable.Ident) = + ident.IsCompilerGenerated + && (ident.Name = "copyOfStruct" || ident.Name = "inputRecord") + + // let isMutableOrByRef (ident: Fable.Ident) = + // (ident.IsMutable || isByRefType ident) + // && not (isTempLocal ident) + let transformIdentGet com ctx r (ident: Fable.Ident) = let expr = transformIdent com ctx r ident @@ -1487,16 +1495,22 @@ module Util = mutableSet expr value let transformIdentType com ctx isCaptured (ident: Fable.Ident) = - let ty = transformType com ctx ident.Type - - if isByRefType com ident.Type then - ty // already wrapped - elif ident.IsMutable && isCaptured then - ty |> makeMutTy com ctx |> makeLrcPtrTy com ctx - elif ident.IsMutable then - ty |> makeMutTy com ctx - else - ty + // let typ = Replacements.Util.getIdentValueType com ident ident.Type + match ident.Type with + | Replacements.Util.IsByRefType com typ when isTempLocal ident -> + // transform temporary local byref type to a reference type + transformType com ctx typ |> mkRefTy None + | _ -> + let ty = transformType com ctx ident.Type + + if isByRefType com ident.Type then + ty // already wrapped + elif ident.IsMutable && isCaptured then + ty |> makeMutTy com ctx |> makeLrcPtrTy com ctx + elif ident.IsMutable then + ty |> makeMutTy com ctx + else + ty let getField r (expr: Rust.Expr) (fieldName: string) = mkFieldExpr expr (fieldName |> sanitizeMember) // ?loc=r) diff --git a/tests/Rust/tests/src/RecordTests.fs b/tests/Rust/tests/src/RecordTests.fs index ce0d9000ab..4913a83613 100644 --- a/tests/Rust/tests/src/RecordTests.fs +++ b/tests/Rust/tests/src/RecordTests.fs @@ -126,14 +126,22 @@ let processStructByValue (s: StructRecord) = [] let ``Struct record works`` () = - let r1 = { i=1; s="hello" } - let r2 = { i=1; s="world" } + let r1 = { i = 1; s = "hello" } + let r2 = { i = 1; s = "world" } let (sres1, ires1) = r1 |> processStructByValue let (sres2, ires2) = r1 |> processStructByValue let (sres3, ires3) = r2 |> processStructByValue //cannot actually test this, but since this is the only reference, the output should not .clone() ires2 |> equal 2 sres3.s |> equal "world" +[] +let ``Struct record temp locals work`` () = + let r1 = { i = 1; s = "hello" } + let r2 = { i = 2; s = "world" } + let r3 = { r1 with i = r2.i ; s = r2.s } + r3.i |> equal 2 + r3.s |> equal "world" + type MutableRecord = { mutable MutValue: int } From f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 10 Oct 2026 17:02:48 +0200 Subject: [PATCH 147/322] fix(beam): normalize exception messages and string split separators (#5053) --- src/Fable.Transforms/Beam/Replacements.fs | 7 +--- src/fable-library-beam/fable_string.erl | 8 ++-- src/fable-library-beam/fable_utils.erl | 6 ++- tests/Beam/ExceptionTests.fs | 48 +++++++++++++++++++++++ tests/Beam/StringTests.fs | 15 +++++++ 5 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 92872cb0d3..8408d3b4d8 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -6190,12 +6190,7 @@ let tryCall "#{exn_type => exception, message => <<\"Exception of type 'System.Exception' was thrown.\">>}" |> Some | "get_Message", Some c, _ -> - // Handle both map exceptions and reference-based class exceptions - emitExpr - r - t - [ c ] - "case erlang:is_reference($0) of true -> maps:get(message, erlang:get($0), $0); false -> maps:get(message, $0, $0) end" + Helper.LibCall(com, "fable_utils", "exception_message", t, [ c ], ?loc = r) |> Some | "get_InnerException", Some c, _ -> // Handle both map exceptions and reference-based class exceptions diff --git a/src/fable-library-beam/fable_string.erl b/src/fable-library-beam/fable_string.erl index a9e8ee0b29..933b72fb51 100644 --- a/src/fable-library-beam/fable_string.erl +++ b/src/fable-library-beam/fable_string.erl @@ -385,14 +385,14 @@ string_ctor_chars_range(Chars, Start, Len) -> normalize_sep(Sep) when is_integer(Sep) -> <>; normalize_sep(Sep) -> Sep. -%% Normalize separator that may be a list of chars to a list of binaries +%% decision: shares separator normalization across split overloads so char codes never reach binary:split normalize_sep_list(Sep) when is_integer(Sep) -> <>; normalize_sep_list(Sep) when is_binary(Sep) -> Sep; normalize_sep_list(Sep) when is_list(Sep) -> - [<> || C <- Sep]. + [normalize_sep(C) || C <- Sep]. split(Str, Sep) -> - binary:split(Str, normalize_sep(Sep), [global]). + binary:split(Str, normalize_sep_list(Sep), [global]). split(Str, Sep, 0) -> split(Str, Sep); @@ -402,7 +402,7 @@ split(Str, Sep, _Options) -> split(Str, Sep). split_remove_empty(Str, Seps) -> - Parts = binary:split(Str, normalize_sep(Seps), [global]), + Parts = binary:split(Str, normalize_sep_list(Seps), [global]), [P || P <- Parts, P =/= <<>>]. split_with_count(Str, Seps, Count) -> diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index 78d1f8337a..bcca5e5d90 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -25,6 +25,7 @@ to_list/1, enumerate_to_list/1, aggregate_exception/2, + exception_message/1, is_exception_type/2, new_byte_array/1, new_byte_array_zeroed/1, @@ -66,6 +67,7 @@ -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). -spec aggregate_exception(binary(), term()) -> map(). +-spec exception_message(term()) -> binary(). -spec is_exception_type(term(), atom()) -> boolean(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). -spec new_byte_array_zeroed(non_neg_integer()) -> tuple(). @@ -519,7 +521,9 @@ exception_type_is(Type, Expected) exception_type_is(system_exception, Expected) -> exception_type_is(exception, Expected); exception_type_is(_, _) -> false. -exception_message(#{message := Message}) -> Message; +%% decision: formats message metadata at the read boundary so exception payload fields retain their original types +%% invariant: Message access and aggregate formatting return binaries for arbitrary F# exception payloads +exception_message(#{message := Message}) -> format_term(Message); exception_message(Exception) when is_reference(Exception) -> exception_message(get(Exception)); exception_message(Exception) -> format_term(Exception). diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index 55b6e145e4..49413902e5 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -6,6 +6,14 @@ open Util.Testing exception MyError of string exception MyError2 of code: int * message: string exception EmptyError +exception NumericError of int + +type ExitReason = + | Stopped + | Failed of string + +exception ExitError of ExitReason +exception NamedNumericError of message: int [] let ``test custom exception can be raised and caught`` () = @@ -73,6 +81,46 @@ let ``test custom exception Message contains field value`` () = // .NET formats as 'MyError "custom msg"', Beam uses the raw field value msg.Contains("custom msg") |> equal true +[] +let ``test numeric exception Message is printable and preserves its payload`` () = + let error = NumericError 42 + let message = error.Message + message.Contains("42") |> equal true + message.Split([| '\n' |]).Length > 0 |> equal true + match error with + | NumericError value -> value |> equal 42 + | _ -> failwith "Expected NumericError" + +[] +let ``test union exception Message is printable and preserves its payload`` () = + let reason = Failed "worker stopped" + let error = ExitError reason + let message = error.Message + message.Contains("worker stopped") |> equal true + message.Split([| '\n' |]).Length > 0 |> equal true + match error with + | ExitError value -> value |> equal reason + | _ -> failwith "Expected ExitError" + +[] +let ``test named numeric exception field stays numeric when Message is read`` () = + let error = NamedNumericError 17 + error.Message.Contains("17") |> equal true + match error with + | NamedNumericError value -> value |> equal 17 + | _ -> failwith "Expected NamedNumericError" + +[] +let ``test aggregate exception formats non-string custom exception messages`` () = + let error = ExitError(Failed "inner worker stopped") + let aggregate = System.AggregateException("outer failure", [| error |]) + aggregate.Message.Contains("outer failure") |> equal true + aggregate.Message.Contains("inner worker stopped") |> equal true + obj.ReferenceEquals(error, aggregate.InnerException) |> equal true + match aggregate.InnerException with + | ExitError(Failed message) -> message |> equal "inner worker stopped" + | _ -> failwith "Expected the original ExitError" + [] let ``test empty custom exception can be caught`` () = let result = diff --git a/tests/Beam/StringTests.fs b/tests/Beam/StringTests.fs index 3cee1934f6..1ecc913dd8 100644 --- a/tests/Beam/StringTests.fs +++ b/tests/Beam/StringTests.fs @@ -311,6 +311,21 @@ let ``test String.Split works`` () = parts.[1] |> equal "b" parts.[2] |> equal "c" +[] +let ``test String.Split with character array separators works`` () = + "one,twoéthree".Split([| ','; 'é' |]) + |> equal [| "one"; "two"; "three" |] + +[] +let ``test String.Split with character array removes empty entries`` () = + " one two ".Split([| ' ' |], StringSplitOptions.RemoveEmptyEntries) + |> equal [| "one"; "two" |] + +[] +let ``test String.Split with string array separators and count works`` () = + "one,two.three".Split([| ","; "." |], 2, StringSplitOptions.None) + |> equal [| "one"; "two.three" |] + [] let ``test String.Split with char works`` () = "a b c d".Split(' ') From 07db576c4ae16dcef9d1773ac4d6c70b2a223f0a Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sat, 10 Oct 2026 17:26:28 +0200 Subject: [PATCH 148/322] fix(js/ts/python): synchronize cancellation registrations (#5036) Co-authored-by: Mangel Maxime --- src/Fable.Build/Test/Python.fs | 4 + src/Fable.Transforms/Python/Replacements.fs | 5 +- src/fable-library-py/fable_library/async_.py | 4 +- .../fable_library/async_builder.py | 109 +++++++--- src/fable-library-ts/Async.ts | 1 + src/fable-library-ts/AsyncBuilder.ts | 25 ++- tests/Js/Main/AsyncTests.fs | 46 +++++ tests/Js/Main/js/async-deferred.js | 15 ++ tests/Python/TestAsync.fs | 25 +++ tests/Python/test_async_runtime.py | 193 ++++++++++++++++++ 10 files changed, 391 insertions(+), 36 deletions(-) create mode 100644 tests/Js/Main/js/async-deferred.js create mode 100644 tests/Python/test_async_runtime.py diff --git a/src/Fable.Build/Test/Python.fs b/src/Fable.Build/Test/Python.fs index 87081b3596..caaaffb692 100644 --- a/src/Fable.Build/Test/Python.fs +++ b/src/Fable.Build/Test/Python.fs @@ -23,6 +23,10 @@ let handle (args: string list) = Directory.clean buildDir + // Native runtime tests use controlled timers and real threads alongside generated F# regressions. + for testFile in Directory.GetFiles(sourceDir, "test_*.py") do + File.Copy(testFile, Path.Combine(buildDir, Path.GetFileName(testFile)), overwrite = true) + Command.Run("uv", "sync") // Install local fable-library as editable package for testing diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 5fb4154f42..85a42fb297 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2851,7 +2851,10 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr Helper.ConstructorCall(e, t, args, ?loc = r) |> Some | "get_Message", Some e -> Helper.GlobalCall("str", t, [ thisArg.Value ], ?loc = r) |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some - | "get_InnerException", Some e -> getFieldWith r t e "inner_exception" |> Some + | "get_InnerException", Some e -> + // Caught exceptions are typed as Python's built-in Exception, which lacks this attribute. + Helper.GlobalCall("getattr", t, [ e; makeStrConst "inner_exception" ], ?loc = r) + |> Some | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 7a5f46a02d..0ef88fb89b 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -59,7 +59,7 @@ def cont(ctx: IAsyncContext[T]): def create_cancellation_token(arg: int | bool | None = None) -> CancellationToken: cancelled = arg if isinstance(arg, bool) else False token = CancellationToken(cancelled) - if isinstance(arg, int): + if isinstance(arg, int) and not isinstance(arg, bool): timer = Timer(arg / 1000.0, token.cancel) timer.start() @@ -113,6 +113,8 @@ def cancel() -> None: ctx.on_cancel(OperationCanceledError()) token_id[0] = ctx.cancel_token.add_listener(cancel) + if ctx.cancel_token.is_cancelled: + return event.AddHandler(handler) return protected_cont(cont) diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index eb098c1c95..30334b32dc 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -4,16 +4,15 @@ from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass, field -from threading import Lock, RLock +from threading import Condition, Lock, RLock, get_ident from typing import ( Any, Literal, - Protocol, overload, ) from .protocols import IEnumerable_1 -from .util import Disposable, IDisposable +from .util import IDisposable class OperationCanceledError(Exception): @@ -28,58 +27,105 @@ def __init__(self, msg: str | None = None) -> None: ] -class _Listener(Protocol): - def __call__(self, __state: Any | None = None) -> None: ... +_NO_STATE = object() + + +class CancellationCallbackError(ExceptionGroup[Exception]): + @property + def inner_exception(self) -> Exception: + # invariant: F# InnerException exposes the first failure without discarding the remaining errors + return self.exceptions[0] class CancellationToken: - __slots__ = "cancelled", "idx", "listeners", "lock" + __slots__ = "_condition", "_running", "cancelled", "idx", "listeners", "lock" - def __init__(self, cancelled: bool = False): + def __init__(self, cancelled: bool = False) -> None: self.cancelled = cancelled self.listeners: dict[int, Callable[[], None]] = {} self.idx = 0 self.lock = RLock() + self._condition = Condition(self.lock) + self._running: dict[int, int] = {} @property - def is_cancelled(self): - return self.cancelled + def is_cancelled(self) -> bool: + with self.lock: + return self.cancelled is_cancellation_requested = is_cancelled def cancel(self) -> None: - cancel = False with self.lock: - if not self.cancelled: - cancel = True - self.cancelled = True + if self.cancelled: + return + self.cancelled = True + # decision: snapshots IDs newest first to match .NET while allowing disposal of pending callbacks + pending = list(reversed(self.listeners)) + + errors: list[Exception] = [] + for listener_id in pending: + try: + self._invoke_listener(listener_id) + except Exception as error: + errors.append(error) + if errors: + raise CancellationCallbackError("Cancellation callbacks failed", errors) - if cancel: - for listener in self.listeners.values(): - listener() + def _invoke_listener(self, listener_id: int) -> None: + with self.lock: + listener = self.listeners.pop(listener_id, None) + if listener is None: + return + self._running[listener_id] = get_ident() + # invariant: user callbacks execute outside the registry lock + try: + listener() + finally: + with self._condition: + del self._running[listener_id] + self._condition.notify_all() def add_listener(self, f: Callable[[], None]) -> int: with self.lock: - id = self.idx - self.idx = self.idx + 1 - self.listeners[id] = f - - return id + listener_id = self.idx + self.idx += 1 + if not self.cancelled: + self.listeners[listener_id] = f + return listener_id + # .NET invokes late registrations synchronously and does not retain them. + f() + return listener_id def remove_listener(self, id: int) -> None: - with self.lock: - del self.listeners[id] + with self._condition: + self.listeners.pop(id, None) + # invariant: disposal waits for a running callback except when that callback disposes itself + while id in self._running and self._running[id] != get_ident(): + self._condition.wait() + + @overload + def register(self, f: Callable[[], None]) -> IDisposable: ... + + @overload + def register[S](self, f: Callable[[S], None], state: S) -> IDisposable: ... + + def register(self, f: Callable[..., None], state: object = _NO_STATE) -> IDisposable: + listener_id = self.add_listener(f if state is _NO_STATE else lambda: f(state)) + return _CancellationRegistration(self, listener_id) + + def Dispose(self) -> None: + # Source disposal is currently a no-op, as in the compiler replacement and JS runtime. + pass - def register(self, f: _Listener, state: Any | None = None) -> None: - if state: - id = self.add_listener(lambda: f(state)) - else: - id = self.add_listener(f) - def dispose(): - self.remove_listener(id) +class _CancellationRegistration: + def __init__(self, token: CancellationToken, listener_id: int) -> None: + self._token = token + self._listener_id = listener_id - Disposable.create(dispose) + def Dispose(self) -> None: + self._token.remove_listener(self._listener_id) class IAsyncContext[T]: @@ -217,6 +263,7 @@ def protected_cont[T](f: Async[T]) -> Async[T]: def _protected_cont(ctx: IAsyncContext[T]): if ctx.cancel_token and ctx.cancel_token.is_cancelled: ctx.on_cancel(OperationCanceledError()) + return def fn(): try: diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index eb0fb63a6b..ec9e58c4aa 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -104,6 +104,7 @@ export function awaitEvent(event: IEvent$2, can if (cancelAction != null) { cancelAction(); } ctx.onCancel(new OperationCanceledException()); }); + if (ctx.cancelToken.isCancelled) { return; } event.AddHandler(handler); }); } diff --git a/src/fable-library-ts/AsyncBuilder.ts b/src/fable-library-ts/AsyncBuilder.ts index 86d59e4ce4..56912579f3 100644 --- a/src/fable-library-ts/AsyncBuilder.ts +++ b/src/fable-library-ts/AsyncBuilder.ts @@ -12,6 +12,16 @@ export type Continuations = [ Continuation ]; +// AggregateError requires ES2021; the runtime also supports ES2020 hosts. +export class CancellationCallbackError extends Exception { + public readonly errors: unknown[]; + constructor(errors: unknown[]) { + // invariant: F# InnerException exposes the first failure without discarding the remaining errors + super("Cancellation callbacks failed", errors[0] as Exception); + this.errors = errors; + } +} + export class CancellationToken implements IDisposable { private _id: number; private _cancelled: boolean; @@ -27,14 +37,23 @@ export class CancellationToken implements IDisposable { public cancel() { if (!this._cancelled) { this._cancelled = true; - for (const [, listener] of this._listeners) { - listener(); + // decision: visit newest registrations first, matching .NET; disposal can skip pending callbacks + const pending = Array.from(this._listeners.keys()).reverse(); + const errors: unknown[] = []; + for (const id of pending) { + const listener = this._listeners.get(id); + this._listeners.delete(id); + if (listener) { + try { listener(); } catch (error) { errors.push(error); } + } } + if (errors.length > 0) { throw new CancellationCallbackError(errors); } } } public addListener(f: () => void) { const id = this._id; - this._listeners.set(this._id++, f); + this._id++; + if (this._cancelled) { f(); } else { this._listeners.set(id, f); } return id; } public removeListener(id: number) { diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 761d32daa4..5127510cf8 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -6,6 +6,13 @@ open Util.Testing #if FABLE_COMPILER open Fable.Core + +type private Deferred = + [] + static member startDeferred(computation: Async, token: System.Threading.CancellationToken, onSuccess: int -> unit, onError: exn -> unit, onCancel: exn -> unit): unit -> unit = jsNative + + [] + static member handlerCount(event: Event): int = jsNative #endif type DisposableAction(f) = @@ -44,6 +51,30 @@ let tests = async { return () } |> Async.StartImmediate + testCase "Cancellation callback errors expose the first InnerException" <| fun () -> + for count in [1; 2] do + use cts = new System.Threading.CancellationTokenSource() + let errors = Array.init count (fun i -> exn (string i)) + let mutable calls = 0 + errors |> Array.iter (fun error -> + cts.Token.Register(fun () -> + calls <- calls + 1 + raise error) |> ignore) + let caught = + try + cts.Cancel() + None + with ex -> Some ex + match caught with + | Some ex -> + equal errors[count - 1].Message ex.InnerException.Message + equal true (obj.ReferenceEquals(errors[count - 1], ex.InnerException)) + | None -> failwith "Expected cancellation callback errors" + equal count calls + equal true cts.IsCancellationRequested + cts.Cancel() + equal count calls + testCase "Async while binding works correctly" <| fun () -> let mutable result = 0 async { @@ -684,6 +715,21 @@ let tests = cts.Cancel() async { equal true cancelCalled } +#if FABLE_COMPILER + testCase "Async.AwaitEvent cancelled before its body runs does not subscribe" <| fun () -> + let ev = Event() + let cts = new System.Threading.CancellationTokenSource() + let calls = ResizeArray() + let resume = + Deferred.startDeferred(Async.AwaitEvent ev.Publish, cts.Token, + (fun v -> calls.Add $"success {v}"), (fun _ -> calls.Add "error"), (fun _ -> calls.Add "cancel")) + cts.Cancel() + resume () + ev.Trigger(42) + equal [ "cancel" ] (List.ofSeq calls) + equal 0 (Deferred.handlerCount ev) +#endif + testCase "Async try .. with does not run 'with' branch when body succeeds" <| fun () -> let work = async { try diff --git a/tests/Js/Main/js/async-deferred.js b/tests/Js/Main/js/async-deferred.js new file mode 100644 index 0000000000..07cb2ad500 --- /dev/null +++ b/tests/Js/Main/js/async-deferred.js @@ -0,0 +1,15 @@ +export function startDeferred(computation, token, onSuccess, onError, onCancel) { + let body; + computation({ + onSuccess, + onError, + onCancel, + cancelToken: token, + trampoline: { completed: false, incrementAndCheck: () => true, hijack: f => { body = f; } }, + }); + return () => body(); +} + +export function handlerCount(event) { + return event.delegates.length; +} diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index 779d7f8653..d6666d7dde 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -32,6 +32,31 @@ let ``test Simple async translates without exception`` () = async { return () } |> Async.StartImmediate +[] +let ``test Cancellation callback errors expose the first InnerException`` () = + for count in [1; 2] do + use cts = new System.Threading.CancellationTokenSource() + let errors = Array.init count (fun i -> exn (string i)) + let mutable calls = 0 + errors |> Array.iter (fun error -> + cts.Token.Register(fun () -> + calls <- calls + 1 + raise error) |> ignore) + let caught = + try + cts.Cancel() + None + with ex -> Some ex + match caught with + | Some ex -> + equal errors[count - 1].Message ex.InnerException.Message + equal true (obj.ReferenceEquals(errors[count - 1], ex.InnerException)) + | None -> failwith "Expected cancellation callback errors" + equal count calls + equal true cts.IsCancellationRequested + cts.Cancel() + equal count calls + [] let ``test Async while binding works correctly`` () = diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py new file mode 100644 index 0000000000..aba116e32e --- /dev/null +++ b/tests/Python/test_async_runtime.py @@ -0,0 +1,193 @@ +import asyncio +from threading import Barrier, Condition, Event, Thread + +import pytest +from fable_library.async_ import await_event, sleep, start_with_continuations +from fable_library.async_builder import CancellationCallbackError, CancellationToken, IAsyncContext, Trampoline +from fable_library.event import Event as FableEvent + + +def test_registration_handle_and_disposal(): + token = CancellationToken() + calls: list[str] = [] + first = token.register(lambda: calls.append("first")) + second = token.register(lambda: calls.append("second")) + assert first is not None + first.Dispose() + first.Dispose() + token.cancel() + second.Dispose() + assert calls == ["second"] + assert not token.listeners + + +def test_late_registration_and_falsy_state(): + token = CancellationToken(True) + calls: list[object] = [] + handle = token.register(lambda state: calls.append(state), 0) + assert calls == [0] + token.register(lambda state: calls.append(state), None).Dispose() + assert calls == [0, None] + handle.Dispose() + assert not token.listeners + + +def test_self_disposal_and_disposal_of_pending_callback(): + token = CancellationToken() + calls: list[str] = [] + first = token.register(lambda: calls.append("first")) + + def callback(): + second.Dispose() + first.Dispose() + calls.append("second") + + second = token.register(callback) + token.cancel() + token.cancel() + assert calls == ["second"] + assert not token.listeners + + +def test_callback_failures_do_not_skip_other_callbacks(): + token = CancellationToken() + calls: list[str] = [] + token.register(lambda: calls.append("other")) + + def failing(): + raise ValueError("callback") + + token.register(failing) + with pytest.raises(ExceptionGroup) as errors: + token.cancel() + assert len(errors.value.exceptions) == 1 + assert calls == ["other"] + assert not token.listeners + + +@pytest.mark.parametrize("count", [1, 2]) +def test_callback_errors_preserve_all_errors_and_first_inner_exception(count: int) -> None: + token = CancellationToken() + errors = [ValueError(str(i)) for i in range(count)] + + def failing(error: Exception) -> None: + raise error + + for error in errors: + token.register(failing, error) + + with pytest.raises(CancellationCallbackError) as caught: + token.cancel() + assert isinstance(caught.value, ExceptionGroup) + assert caught.value.exceptions == tuple(reversed(errors)) + assert caught.value.inner_exception is errors[-1] + assert not token.listeners + token.cancel() + + +def test_dispose_waits_for_running_callback_and_late_registration_runs_outside_lock(): + token = CancellationToken() + entered, release, disposed, late = Event(), Event(), Event(), Event() + + def callback(): + entered.set() + assert release.wait(5) + + waiting = Event() + + class ObservedCondition(Condition): + def wait(self, timeout=None): + waiting.set() + return super().wait(timeout) + + token._condition = ObservedCondition(token.lock) + handle = token.register(callback) + cancel_thread = Thread(target=token.cancel) + cancel_thread.start() + assert entered.wait(5) + + # Registration after cancellation must run immediately, even while an earlier callback is blocked. + register_thread = Thread(target=lambda: token.register(late.set).Dispose()) + register_thread.start() + assert late.wait(5) + register_thread.join(5) + + def dispose(): + handle.Dispose() + disposed.set() + + dispose_thread = Thread(target=dispose) + dispose_thread.start() + assert waiting.wait(5) + assert not disposed.is_set() + release.set() + cancel_thread.join(5) + dispose_thread.join(5) + assert disposed.is_set() + assert not token.listeners + + +def test_concurrent_registration_and_cancellation(): + token = CancellationToken() + gate = Barrier(17) + calls = [0] * 16 + + def register(index: int): + gate.wait(5) + + def callback(): + calls[index] += 1 + + token.register(callback).Dispose() + + threads = [Thread(target=register, args=(i,)) for i in range(16)] + for thread in threads: + thread.start() + gate.wait(5) + token.cancel() + for thread in threads: + thread.join(5) + assert not thread.is_alive() + assert all(count in (0, 1) for count in calls) + assert not token.listeners + assert not token._running + + +def test_sleep_on_cancelled_token_settles_once(): + calls: list[str] = [] + start_with_continuations( + sleep(10), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + CancellationToken(True), + ) + assert calls == ["cancel"] + + +class _DeferringTrampoline(Trampoline): + def increment_and_check(self) -> bool: + return True + + +def test_await_event_cancelled_before_its_body_runs_does_not_subscribe(): + event: FableEvent[int] = FableEvent() + token = CancellationToken() + calls: list[str] = [] + + async def run() -> None: + ctx = IAsyncContext.create( + _DeferringTrampoline(), + token, + lambda value: calls.append(f"success {value}"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + ) + await_event(event)(ctx) + token.cancel() + await asyncio.sleep(0) + + asyncio.run(run()) + event.Trigger(42) + assert calls == ["cancel"] + assert not event.delegates From 10fdccc04bb0e421643426d21dfcbd940e3e5a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:54:30 -0700 Subject: [PATCH 149/322] fix(rust): Add main function for no_std (#5054) --- .../Rust/AST/Rust.AST.Helpers.fs | 18 +++++ src/Fable.Transforms/Rust/Fable2Rust.fs | 77 +++++++++++++++---- 2 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs index 2fafbb99ee..ed695d9aa7 100644 --- a/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs +++ b/src/Fable.Transforms/Rust/AST/Rust.AST.Helpers.fs @@ -1123,6 +1123,24 @@ module Types = let mkParenTy ty : Ty = TyKind.Paren(ty) |> mkTy + let mkPtrTy ty : Ty = + TyKind.Ptr( + { + ty = ty + mutbl = Mutability.Not + } + ) + |> mkTy + + let mkMutPtrTy ty : Ty = + TyKind.Ptr( + { + ty = ty + mutbl = Mutability.Mut + } + ) + |> mkTy + let mkRefTy (lifetimeOpt: Symbol option) ty : Ty = let opt_lifetime = lifetimeOpt |> Option.map mkLifetime diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index a86bfda20d..6c0b7943cf 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -4276,26 +4276,72 @@ module Util = match entryPoint with | Some path -> // add some imports for main function - let asArr = getLibraryImportName com ctx "NativeArray" "array_from" - let asStr = getLibraryImportName com ctx "String" "fromString" + let ofArr = getLibraryImportName com ctx "NativeArray" "array_from" + let ofStr = getLibraryImportName com ctx "String" "fromString" + let asStr = getLibraryImportName com ctx "String" "string" // main entrypoint let mainName = String.concat "::" path - let strBody = - [ - $"let args = std::env::args().skip(1).map(%s{asStr}).collect()" - $"%s{mainName}(%s{asArr}(args))" - ] + // main function + let mainFnItem = + let strBody = + [ + $"let iter = std::env::args()" + $"let args = iter.skip(1).map(%s{ofStr}).collect()" + $"%s{mainName}(%s{ofArr}(args))" + ] + + let fnBody = strBody |> Seq.map mkEmitSemiStmt |> mkBlock |> Some + + let attrs = [ mkAttr "cfg" [ "not(feature = \"no_std\")" ] ] + let fnDecl = mkFnDecl [] VOID_RETURN_TY + let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS fnBody + let fnItem = mkFnItem attrs "main" fnKind + fnItem |> mkPublicItem + + // main function (no_std) + let mainFnItemNoStd = + let strBody = + [ + $"let iter = fable_library_rust::Native_::get_args(argc, argv)" + $"let args = iter.skip(1).map(%s{asStr}).collect()" + $"%s{mainName}(%s{ofArr}(args)) as u32" + ] + + let lastStatementIndex = List.length strBody - 1 - let fnBody = strBody |> Seq.map mkEmitSemiStmt |> mkBlock |> Some + let fnBody = + strBody + |> List.mapi (fun index statement -> + if index = lastStatementIndex then + mkEmitExprStmt statement + else + mkEmitSemiStmt statement + ) + |> mkBlock + |> Some - let attrs = [ mkAttr "cfg" [ "not(feature = \"no_std\")" ] ] - let fnDecl = mkFnDecl [] VOID_RETURN_TY - let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl NO_GENERICS fnBody - let fnItem = mkFnItem attrs "main" fnKind - [ fnItem |> mkPublicItem ] + let attrs = + [ mkAttr "cfg" [ "feature = \"no_std\"" ]; mkAttr "r#unsafe" [ "no_mangle" ] ] + + let argcTy = primitiveType "isize" + let argvTy = primitiveType "u8" |> mkPtrTy |> mkPtrTy + let retTy = primitiveType "u32" |> mkFnRetTy + + let args = + [ + mkTypedParam "argc" argcTy false false + mkTypedParam "argv" argvTy false false + ] + let fnHeader = mkFnHeader false false false (Some "C") + let fnDecl = mkFnDecl args retTy + let fnKind = mkFnKind fnHeader fnDecl NO_GENERICS fnBody + let fnItem = mkFnItem attrs "main" fnKind + fnItem |> mkPublicItem + + [ mainFnItem; mainFnItemNoStd ] | None -> [] let getEntityPhantomGenParams _com (ent: Fable.Entity) : (string * Fable.Type) list = @@ -6156,13 +6202,15 @@ module Util = | Fable.ClassDeclaration decl -> transformClassDecl com ctx decl let transformDeclarations (com: IRustCompiler) ctx decls = + let useItem = mkGlobUseItem [] [ "super" ] + let items = decls |> mergeNamespaceDecls com ctx |> List.collect (transformDecl com ctx) // wrap the last file in a module to consistently handle namespaces if isLastFileInProject com then let modPath = fixFileExtension com com.CurrentFile let modName = getImportModuleName com modPath - let modItem = mkModItem [] modName items + let modItem = mkModItem [] modName (useItem :: items) let useItem = mkGlobUseItem [] [ modName ] [ modItem; useItem |> mkPublicItem ] @@ -6437,6 +6485,7 @@ module Compiler = if isLastFileInProject com then // adds "no_std" to crate if feature is enabled mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_std" ] + mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_main" ] // TODO: make some of those conditional on compiler options mkInnerAttr "allow" [ "dead_code" ] From e05bc752f1d8f16f08e2409a191d9c33463a1f04 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Sun, 11 Oct 2026 10:58:02 +0200 Subject: [PATCH 150/322] fix(js/ts/python): preserve JS/TS generic unit arguments and Python unit continuations (#5037) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Babel/Fable2Babel.fs | 59 +++++++++++++++--- .../Python/Fable2Python.Transforms.fs | 11 +++- src/Fable.Transforms/Python/Replacements.fs | 7 +++ src/fable-library-py/fable_library/async_.py | 15 +++++ tests/Beam/AsyncTests.fs | 22 +++++++ tests/Js/Main/AsyncTests.fs | 61 +++++++++++++++++++ tests/Python/TestAsync.fs | 28 +++++++++ 7 files changed, 191 insertions(+), 12 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 89ad79c781..f72d32e3a7 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -47,6 +47,7 @@ type Context = TailCallOpportunity: ITailCallOpportunity option OptimizeTailCall: unit -> unit ScopedTypeParams: Set + BoundTypes: Map ForcedIdents: Set } @@ -2451,17 +2452,35 @@ but thanks to the optimisation done below we get | Some(TransformExpr com ctx thisArg) -> callFunction com ctx range callee callInfo.GenericArgs (thisArg :: args) - let transformCurriedApply com ctx range (TransformExpr com ctx applied) args = - (applied, args) - ||> List.fold (fun expr arg -> - match arg with - // TODO: If arg type is unit but it's an expression with potential - // side-effects, we need to extract it and execute it before the call - | Fable.Value(Fable.UnitConstant, _) -> [] - | Fable.IdentExpr ident when ident.Type = Fable.Unit -> [] - | TransformExpr com ctx arg -> [ arg ] - |> callFunction com ctx range expr [] + let transformCurriedApply (com: IBabelCompiler) ctx range (applied: Fable.Expr) args = + // decision: declaration types preserve generic unit arguments despite specialized identifier types at call sites + let appliedType = + match applied with + // invariant: FSharp2Fable makes local names unique within a declaration (getIdentUniqueName) + | MaybeCasted(Fable.IdentExpr id) -> Map.tryFind id.Name ctx.BoundTypes |> Option.defaultValue id.Type + | _ -> applied.Type + + let applied = com.TransformAsExpr(ctx, applied) + + ((applied, appliedType), args) + ||> List.fold (fun (expr, calleeType) arg -> + let argType, returnType = + match calleeType with + | Fable.LambdaType(argType, returnType) -> Some argType, returnType + | _ -> None, Fable.Any + + let args = + match arg, argType with + | _, Some argType when argType <> Fable.Unit -> [ com.TransformAsExpr(ctx, arg) ] + // TODO: If arg type is unit but it's an expression with potential + // side-effects, we need to extract it and execute it before the call + | Fable.Value(Fable.UnitConstant, _), _ -> [] + | Fable.IdentExpr ident, _ when ident.Type = Fable.Unit -> [] + | TransformExpr com ctx arg, _ -> [ arg ] + + callFunction com ctx range expr [] args, returnType ) + |> fst let transformCallAsStatements com ctx range typ returnStrategy callee callInfo = let argsLen (i: Fable.CallInfo) = @@ -3197,6 +3216,7 @@ but thanks to the optimisation done below we get | Fable.Let(ident, value, body) -> if ctx.HoistVars [ ident ] then let assignment = transformBindingAsExpr com ctx ident value + let ctx = { ctx with BoundTypes = Map.add ident.Name ident.Type ctx.BoundTypes } Expression.sequenceExpression ([| assignment; com.TransformAsExpr(ctx, body) |]) else @@ -3204,6 +3224,13 @@ but thanks to the optimisation done below we get | Fable.LetRec(bindings, body) -> if ctx.HoistVars(List.map fst bindings) then + let ctx = + { ctx with + BoundTypes = + (ctx.BoundTypes, bindings) + ||> List.fold (fun types (id, _) -> Map.add id.Name id.Type types) + } + let values = bindings |> List.mapToArray (fun (id, value) -> transformBindingAsExpr com ctx id value) @@ -3310,10 +3337,18 @@ but thanks to the optimisation done below we get | Fable.Let(ident, value, body) -> let binding = transformBindingAsStatements com ctx ident value + let ctx = { ctx with BoundTypes = Map.add ident.Name ident.Type ctx.BoundTypes } Array.append binding (transformAsStatements com ctx returnStrategy body) | Fable.LetRec(bindings, body) -> + let ctx = + { ctx with + BoundTypes = + (ctx.BoundTypes, bindings) + ||> List.fold (fun types (id, _) -> Map.add id.Name id.Type types) + } + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) @@ -3419,6 +3454,9 @@ but thanks to the optimisation done below we get { ctx with TailCallOpportunity = tailcallChance OptimizeTailCall = fun () -> isTailCallOptimized <- true + BoundTypes = + (ctx.BoundTypes, args) + ||> List.fold (fun types id -> Map.add id.Name id.Type types) } let returnStrategy = @@ -5052,6 +5090,7 @@ module Compiler = TailCallOpportunity = None OptimizeTailCall = fun () -> () ScopedTypeParams = Set.empty + BoundTypes = Map.empty ForcedIdents = Set.empty } diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index cfbe4a9ebd..d0bb7480da 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3374,8 +3374,15 @@ let transformFunction args = [ Arg.arg (Identifier("__unit"), annotation = unitType) ], defaults = [ unitDefault ] ) - // No args but has tail-call args: skip __unit, tcArgs are sufficient - | [], _, _ -> Arguments.arguments (args = tcArgs, defaults = tcDefaults) + // invariant: a runtime unit argument never overwrites a captured TCO default + | [], _, _ -> + let unitType = com.GetImportExpr(ctx, getLibPath com "util", "Unit") + let unitName = getUniqueNameInDeclarationScope ctx "__unit" + + Arguments.arguments ( + args = Arg.arg (Identifier unitName, annotation = unitType) :: tcArgs, + defaults = unitDefault :: tcDefaults + ) // Single generic/unit arg with no tail-call args: keep it with () default | [ arg ], true, [] -> Arguments.arguments (args = args, defaults = [ unitDefault ]) // Single generic/unit arg with tail-call args: keep arg (body may reference it) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 85a42fb297..1513e1a161 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3675,6 +3675,13 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with + | "FromContinuations" when + (match t with + | Fable.DeclaredType(_, [ Fable.Unit ]) -> true + | _ -> false) + -> + Helper.LibCall(com, "async_", "from_continuations_unit", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | "Start" -> Helper.LibCall(com, "async_", "start", t, args, i.SignatureArgTypes, ?loc = r) |> Some diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 0ef88fb89b..faed9113f8 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -188,6 +188,21 @@ def cont(ctx: IAsyncContext[T]) -> None: return protected_cont(cont) +def from_continuations_unit( + f: Callable[ + [tuple[Callable[[], None], Callable[[Exception], None], Callable[[OperationCanceledError], None]]], + None, + ], +) -> Async[None]: + def cont(ctx: IAsyncContext[None]) -> None: + def success(value: None = None) -> None: + ctx.on_success(value) + + f((success, ctx.on_error, ctx.on_cancel)) + + return protected_cont(cont) + + def await_task[T](task: Awaitable[T]) -> Async[T]: """Return an asynchronous computation that will wait for the given task to complete and return its result. diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index f1ebea0b34..c2011b6662 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -757,3 +757,25 @@ let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = equal true cancelCalled #endif +[] +let ``test FromContinuations unit and generic values`` () = + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + +[] +let ``test unit continuations preserve captured callbacks`` () = + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + equal 2 calls diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 5127510cf8..448326ad15 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -47,6 +47,67 @@ let asyncMap f a = async { let tests = testList "Async" [ + testCase "FromContinuations unit and generic values" <| fun () -> + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + + testCase "Generic unit continuations work through recursive bindings and aliases" <| fun () -> + let mutable recurse = true + let rec fromValue value = + if recurse then + recurse <- false + fromValue value + else Async.FromContinuations(fun (ok, _, _) -> ok value) + let alias = fromValue + let mutable completed = 0 + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + recurse <- true + Async.StartWithContinuations(alias (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(alias 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + + testCase "Generic unit continuations work through tuple pattern bindings" <| fun () -> + let fromValue, fromOther = + (fun (value: 'T) -> Async.FromContinuations(fun (ok, _, _) -> ok value)), + (fun (value: 'U) -> Async.FromContinuations(fun (ok, _, _) -> ok value)) + let mutable completed = 0 + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromOther (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromOther "a", (fun value -> equal "a" value; completed <- completed + 1), raise, raise) + equal 4 completed + + testCase "Generic unit continuations work inside nested lambdas and object expressions" <| fun () -> + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let startUnit () = Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + let disposable = + { new IDisposable with + member _.Dispose() = + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) } + startUnit () + startUnit () + disposable.Dispose() + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 4 completed + + testCase "Unit continuations preserve captured callbacks" <| fun () -> + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + equal 2 calls + testCase "Simple async translates without exception" <| fun () -> async { return () } |> Async.StartImmediate diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index d6666d7dde..cb701222d1 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -678,3 +678,31 @@ let ``test Async.AwaitEvent with cancelAction invokes it on cancellation`` () = }, cts.Token) cts.Cancel() equal true cancelCalled + +[] +let ``test FromContinuations unit and generic values`` () = + let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) + let mutable completed = 0 + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok ()) + Async.StartWithContinuations(unitWork, (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue (), (fun () -> completed <- completed + 1), raise, raise) + Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) + equal 3 completed + +[] +let ``test unit continuations preserve captured callbacks`` () = + let mutable calls = 0 + let rec genericDeadline callback value remaining = + if remaining > 0 then genericDeadline callback value (remaining - 1) + else Async.StartWithContinuations(async { return value }, (fun result -> callback result), raise, raise) + let rec unitDeadline (callback: unit -> unit) remaining = + if remaining > 0 then unitDeadline callback (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> callback ()), raise, raise) + genericDeadline (fun () -> calls <- calls + 1) () 2 + unitDeadline (fun () -> calls <- calls + 1) 2 + let rec namedUnitCapture (__unit: int) remaining = + if remaining > 0 then namedUnitCapture __unit (remaining - 1) + else Async.StartWithContinuations(async { return () }, (fun () -> calls <- calls + __unit), raise, raise) + namedUnitCapture 3 2 + equal 5 calls + From 380e2135746867373a188aba2d13c4fb02b15682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:39:11 -0700 Subject: [PATCH 151/322] fix(rust): No main in tests for xunit v3 (#5055) --- tests/Rust/Cargo.toml | 4 ++++ tests/Rust/Fable.Tests.Rust.fsproj | 2 +- tests/Rust/tests/src/{main.fs => lib.fs} | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) rename tests/Rust/tests/src/{main.fs => lib.fs} (63%) diff --git a/tests/Rust/Cargo.toml b/tests/Rust/Cargo.toml index fdd2cb5692..519c99b520 100644 --- a/tests/Rust/Cargo.toml +++ b/tests/Rust/Cargo.toml @@ -3,6 +3,10 @@ name = "fable_tests_rust" version = "0.1.0" edition = "2024" +[lib] +name = "fable_tests_rust" +path = "tests/src/lib.rs" + [features] lrc_ptr = ["fable_library_rust/lrc_ptr"] no_std = ["fable_library_rust/no_std"] diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 8e22dc06c8..2b9a48b8ba 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -85,6 +85,6 @@ - + diff --git a/tests/Rust/tests/src/main.fs b/tests/Rust/tests/src/lib.fs similarity index 63% rename from tests/Rust/tests/src/main.fs rename to tests/Rust/tests/src/lib.fs index 6a938fa462..e448cd8f1f 100644 --- a/tests/Rust/tests/src/main.fs +++ b/tests/Rust/tests/src/lib.fs @@ -6,6 +6,6 @@ let _imports() = importAll "./ExtInteropTests.rs" () -// [] +// Comment the main func when no longer needed (after migrating to xunit v3) [] let main _args = 0 From a9802484c8cdeadd5203123d76f42e375c1c3159 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 12 Oct 2026 15:50:56 +0200 Subject: [PATCH 152/322] fix(js/ts/python): settle cancelled delays and finalizers once (#5038) Co-authored-by: Mangel Maxime --- src/fable-library-py/fable_library/async_.py | 73 +++++- .../fable_library/async_builder.py | 22 +- .../fable_library/mailbox_processor.py | 61 +++-- src/fable-library-ts/Async.ts | 19 +- src/fable-library-ts/AsyncBuilder.ts | 13 +- src/fable-library-ts/MailboxProcessor.ts | 21 +- tests/Js/Main/AsyncTests.fs | 162 ++++++++++++ tests/Js/Main/js/async-runtime.js | 36 +++ tests/Python/TestAsync.fs | 243 +++++++++++++++++ tests/Python/py/async_runtime.py | 248 ++++++++++++++++++ tests/Python/test_async_runtime.py | 25 ++ 11 files changed, 868 insertions(+), 55 deletions(-) create mode 100644 tests/Js/Main/js/async-runtime.js create mode 100644 tests/Python/py/async_runtime.py diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index faed9113f8..ff1a7a03f6 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -4,7 +4,7 @@ from asyncio import Future, ensure_future from collections.abc import Awaitable, Callable, Iterable from concurrent.futures import ThreadPoolExecutor -from threading import Timer +from threading import RLock, Timer from typing import ( Any, ) @@ -80,19 +80,70 @@ def is_cancellation_requested(token: CancellationToken) -> bool: return token.is_cancelled -def sleep(milliseconds_duetime: int | TimeSpan) -> Async[None]: - def cont(ctx: IAsyncContext[None]): - def cancel(): - ctx.on_cancel(OperationCanceledError()) +def call_on_owner_loop(loop: asyncio.AbstractEventLoop, action: Callable[[], None]) -> None: + # decision: foreign-thread cancellation settles on the owning loop while it is running + if loop.is_closed() or not loop.is_running(): + action() + return + try: + current_loop = asyncio.get_running_loop() + except RuntimeError: + current_loop = None + if current_loop is loop: + action() + return + try: + loop.call_soon_threadsafe(action) + except RuntimeError: + if not loop.is_closed(): + raise + action() - token_id = ctx.cancel_token.add_listener(cancel) - def timeout(): - ctx.cancel_token.remove_listener(token_id) - ctx.on_success(None) +def sleep(milliseconds_duetime: int | TimeSpan) -> Async[None]: + def cont(ctx: IAsyncContext[None]) -> None: + loop = asyncio.get_running_loop() + gate = RLock() + completed = False + timer: asyncio.TimerHandle | None = None + token_id: int | None = None + + def settle(cancelled: bool) -> None: + nonlocal completed + with gate: + if completed: + return + cancelled = cancelled or ctx.cancel_token.is_cancelled + completed = True + if timer is not None: + timer.cancel() + listener_id = token_id + # Dispose outside the gate: another thread may already be running cancel(). + if listener_id is not None: + ctx.cancel_token.remove_listener(listener_id) + if cancelled: + ctx.on_cancel(OperationCanceledError()) + else: + ctx.on_success(None) - due_time = to_milliseconds(milliseconds_duetime) / 1000.0 - ctx.trampoline.run_later(timeout, due_time) + def cancel() -> None: + call_on_owner_loop(loop, lambda: settle(True)) + + # invariant: installation and settlement cannot interleave before both resources are owned + try: + with gate: + token_id = ctx.cancel_token.add_listener(cancel) + if not completed: + due_time = to_milliseconds(milliseconds_duetime) / 1000.0 + timer = ctx.trampoline.run_later(lambda: settle(False), due_time) + if completed: + timer.cancel() + except Exception: + with gate: + completed = True + if token_id is not None: + ctx.cancel_token.remove_listener(token_id) + raise return protected_cont(cont) diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index 30334b32dc..377aa71481 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -245,9 +245,9 @@ def run_later( self, action: Callable[[], None], due_time: float = 0.0, - ): + ) -> asyncio.TimerHandle: loop = asyncio.get_running_loop() - loop.call_later(due_time, action) + return loop.call_later(due_time, action) def run(self, action: Callable[[], None]): loop = asyncio.get_running_loop() @@ -345,15 +345,27 @@ def ReturnFrom[T](self, computation: Async[T]) -> Async[T]: def TryFinally[T](self, computation: Async[T], compensation: Callable[[], None]) -> Async[T]: def cont(ctx: IAsyncContext[T]) -> None: def on_success(x: T) -> None: - compensation() + try: + compensation() + except Exception as error: + ctx.on_error(error) + return ctx.on_success(x) def on_error(x: Exception) -> None: - compensation() + try: + compensation() + except Exception as error: + ctx.on_error(error) + return ctx.on_error(x) def on_cancel(x: OperationCanceledError) -> None: - compensation() + # decision: ignores compensation failures during cancellation, matching .NET Async.TryFinally + try: + compensation() + except Exception: + pass ctx.on_cancel(x) ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, on_success, on_error, on_cancel) diff --git a/src/fable-library-py/fable_library/mailbox_processor.py b/src/fable-library-py/fable_library/mailbox_processor.py index 663635f589..cbdbe5f020 100644 --- a/src/fable-library-py/fable_library/mailbox_processor.py +++ b/src/fable-library-py/fable_library/mailbox_processor.py @@ -1,16 +1,19 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from queue import SimpleQueue from threading import RLock from typing import Any -from .async_ import from_continuations, start_immediate +from .async_ import call_on_owner_loop, from_continuations, start_immediate from .async_builder import ( Async, CancellationToken, Continuations, + IAsyncContext, OperationCanceledError, + protected_cont, ) @@ -101,34 +104,44 @@ def receive(self) -> Async[Msg]: Returns: An asynchronous computation which will consume the first message in arrival order. No thread is blocked while - waiting for further messages. Raises a TimeoutException if - the timeout is exceeded. + waiting for further messages. Cancelling the token of the + receiving computation cancels a pending receive. """ - def callback(conts: Continuations[Any]): - if self.continuation: - raise Exception("Receive can only be called once!") - - self.continuation = conts - + def cont(ctx: IAsyncContext[Msg]) -> None: + loop = asyncio.get_running_loop() + token_id: int | None = None + + def on_message(msg: Msg) -> None: + if token_id is not None: + ctx.cancel_token.remove_listener(token_id) + ctx.on_success(msg) + + continuation: Continuations[Any] = (on_message, ctx.on_error, ctx.on_cancel) + + def on_cancel() -> None: + with self.lock: + if self.continuation is not continuation: + return + self.continuation = None + call_on_owner_loop(loop, lambda: ctx.on_cancel(OperationCanceledError())) + + with self.lock: + if self.continuation: + raise Exception("Receive can only be called once!") + self.continuation = continuation + + token_id = ctx.cancel_token.add_listener(on_cancel) + with self.lock: + if self.continuation is not continuation: + return self.__process_events() - return from_continuations(callback) + return protected_cont(cont) def __process_events(self) -> None: - if not self.continuation: - return - - # Cancellation of async workflows is more tricky in Python than - # with F# so we check the cancellation token for each process. - if self.token.is_cancellation_requested: - self.continuation, cont = None, self.continuation - if cont: - cont[2](OperationCanceledError("Mailbox was cancelled")) - return - with self.lock: - if self.messages.empty(): + if not self.continuation or self.messages.empty(): return msg = self.messages.get() self.continuation, cont = None, self.continuation @@ -143,7 +156,7 @@ def start( cancellation_token: CancellationToken | None = None, ) -> MailboxProcessor[Msg]: mbox: MailboxProcessor[Msg] = MailboxProcessor(body, cancellation_token) - start_immediate(body(mbox)) + start_immediate(body(mbox), mbox.token) return mbox @@ -163,7 +176,7 @@ def post_and_async_reply[Msg, Reply]( def start_instance(mbox: MailboxProcessor[Any]) -> None: body = mbox.body(mbox) - return start_immediate(body) + return start_immediate(body, mbox.token) def start[Msg]( diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index ec9e58c4aa..23ab998b7e 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -159,15 +159,18 @@ export function sequential(computations: Iterable>) { export function sleep(millisecondsDueTime: number) { return protectedCont((ctx: IAsyncContext) => { - let tokenId: number; - const timeoutId = setTimeout(() => { - ctx.cancelToken.removeListener(tokenId); - ctx.onSuccess(void 0); - }, millisecondsDueTime); - tokenId = ctx.cancelToken.addListener(() => { + let completed = false; + let tokenId: number | undefined; + const settle = (cancelled: boolean) => { + if (completed) { return; } + completed = true; clearTimeout(timeoutId); - ctx.onCancel(new OperationCanceledException()); - }); + if (tokenId !== undefined) { ctx.cancelToken.removeListener(tokenId); } + if (cancelled) { ctx.onCancel(new OperationCanceledException()); } + else { ctx.onSuccess(void 0); } + }; + const timeoutId = setTimeout(() => settle(false), millisecondsDueTime); + tokenId = ctx.cancelToken.addListener(() => settle(true)); }); } diff --git a/src/fable-library-ts/AsyncBuilder.ts b/src/fable-library-ts/AsyncBuilder.ts index 56912579f3..2a89be285e 100644 --- a/src/fable-library-ts/AsyncBuilder.ts +++ b/src/fable-library-ts/AsyncBuilder.ts @@ -203,15 +203,22 @@ export class AsyncBuilder { return protectedCont((ctx: IAsyncContext) => { computation({ onSuccess: (x: T) => { - compensation(); + try { compensation(); } catch (error) { + ctx.onError(ensureErrorOrException(error)); + return; + } ctx.onSuccess(x); }, onError: (x: any) => { - compensation(); + try { compensation(); } catch (error) { + ctx.onError(ensureErrorOrException(error)); + return; + } ctx.onError(x); }, onCancel: (x: any) => { - compensation(); + // decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally + try { compensation(); } catch { /* Cancellation takes precedence. */ } ctx.onCancel(x); }, cancelToken: ctx.cancelToken, diff --git a/src/fable-library-ts/MailboxProcessor.ts b/src/fable-library-ts/MailboxProcessor.ts index b78689d7c7..b1771bf70a 100644 --- a/src/fable-library-ts/MailboxProcessor.ts +++ b/src/fable-library-ts/MailboxProcessor.ts @@ -1,7 +1,7 @@ import { defaultCancellationToken } from "./Async.ts"; import { fromContinuations } from "./Async.ts"; import { startImmediate } from "./Async.ts"; -import { Async } from "./AsyncBuilder.ts"; +import { Async, IAsyncContext, OperationCanceledException, protectedCont } from "./AsyncBuilder.ts"; import { Continuation, Continuations } from "./AsyncBuilder.ts"; import { CancellationToken } from "./AsyncBuilder.ts"; import { Exception } from "./Util.ts"; @@ -80,12 +80,25 @@ export function startInstance($this: MailboxProcessor) { } export function receive($this: MailboxProcessor) { - return fromContinuations((conts: Continuations) => { + return protectedCont((ctx: IAsyncContext) => { if ($this.continuation) { throw new Exception("Receive can only be called once!"); } - $this.continuation = conts[0]; - __processEvents($this); + let tokenId: number | undefined; + const continuation = (message: Msg) => { + if (tokenId !== undefined) { ctx.cancelToken.removeListener(tokenId); } + ctx.onSuccess(message); + }; + $this.continuation = continuation; + tokenId = ctx.cancelToken.addListener(() => { + if ($this.continuation === continuation) { + delete $this.continuation; + ctx.onCancel(new OperationCanceledException()); + } + }); + if ($this.continuation === continuation) { + __processEvents($this); + } }); } diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 448326ad15..362207952e 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -13,6 +13,18 @@ type private Deferred = [] static member handlerCount(event: Event): int = jsNative + +module private Runtime = + open Fable.Core.JsInterop + + type Timer = + abstract retired: int + abstract invalidHandles: int + abstract fire: unit -> unit + + let withTimers (action: Timer -> unit): unit = importMember "./js/async-runtime.js" + let listenerCount (token: System.Threading.CancellationToken): int = importMember "./js/async-runtime.js" + let cancellationErrorCount (error: exn): int = importMember "./js/async-runtime.js" #endif type DisposableAction(f) = @@ -108,6 +120,156 @@ let tests = unitDeadline (fun () -> calls <- calls + 1) 2 equal 2 calls + testCase "Finalizer failure overrides success once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! successWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Finalizer failure overrides body failure once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! errorWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Finalizer failure preserves cancellation once" <| fun () -> + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! cancelWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel")) + equal [| "cancel" |] (terminal.ToArray()) + equal 1 finalizers + + testCase "Cancellation registrations dispose pending callbacks and invoke late callbacks" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let calls = ResizeArray() + let first: IDisposable = source.Token.Register(fun () -> calls.Add 1) + let mutable second: IDisposable = null + second <- source.Token.Register(fun () -> + second.Dispose() + first.Dispose() + calls.Add 2) + source.Cancel() + let late: IDisposable = source.Token.Register((fun state -> calls.Add (unbox state)), box 0) + late.Dispose() + second.Dispose() + equal [| 2; 0 |] (calls.ToArray()) +#if FABLE_COMPILER + equal 0 (Runtime.listenerCount source.Token) +#endif + + testCase "Cancellation callback failure preserves remaining listeners" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let mutable remaining = 0 + let mutable errors = -1 + source.Token.Register(fun () -> remaining <- remaining + 1) |> ignore + source.Token.Register(fun () -> failwith "callback") |> ignore + try source.Cancel() + with error -> +#if FABLE_COMPILER + errors <- Runtime.cancellationErrorCount error +#else + errors <- (error :?> AggregateException).InnerExceptions.Count +#endif + equal 1 errors + equal 1 remaining +#if FABLE_COMPILER + equal 0 (Runtime.listenerCount source.Token) +#endif + +#if FABLE_COMPILER + testCase "Sleep settles once and retires its timer and listener" <| fun () -> + for cancelFirst in [ true; false ] do + for throws in [ true; false ] do + Runtime.withTimers (fun timer -> + use source = new System.Threading.CancellationTokenSource() + let terminal = ResizeArray() + let mutable finalizers = 0 + let work = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + if throws then failwith "finalizer" + } + Async.StartWithContinuations(work, (fun () -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel"), source.Token) + if cancelFirst then + source.Cancel() + timer.fire() + else + timer.fire() + source.Cancel() + timer.fire() + let expected = if cancelFirst then "cancel" elif throws then "error" else "success" + equal [| expected |] (terminal.ToArray()) + equal 1 finalizers + equal 0 (Runtime.listenerCount source.Token) + equal 1 timer.retired + equal 0 timer.invalidHandles) + + testCase "Idle Receive is cancelled by its token and keeps later posts queued" <| fun () -> + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let terminal = ResizeArray() + Async.StartWithContinuations(mailbox.Receive(), (fun value -> terminal.Add (string value)), raise, (fun _ -> terminal.Add "cancel"), source.Token) + source.Cancel() + equal [| "cancel" |] (terminal.ToArray()) + equal 0 (Runtime.listenerCount source.Token) + mailbox.Post 42 + equal [| "cancel" |] (terminal.ToArray()) + Async.StartWithContinuations(mailbox.Receive(), (fun value -> terminal.Add $"next {value}"), raise, (fun _ -> terminal.Add "next cancel")) + equal [| "cancel"; "next 42" |] (terminal.ToArray()) +#endif + + testCaseAsync "Cancelling a waiting Receive reports cancel and keeps later posts queued" <| fun () -> async { + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + Async.StartWithContinuations(mailbox.Receive(), (fun value -> ok $"value {value}"), (fun _ -> ok "error"), (fun _ -> ok "cancel"), source.Token) + source.Cancel()) + equal "cancel" outcome + mailbox.Post 42 + let! next = mailbox.Receive() + equal 42 next + } + + testCaseAsync "Cancelling the mailbox token cancels a body waiting in Receive" <| fun () -> async { + use source = new System.Threading.CancellationTokenSource() + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + MailboxProcessor.Start((fun inbox -> async { + let received = ref None + try + source.CancelAfter 50 + let! message = inbox.Receive() + received.Value <- Some message + finally + match received.Value with + | Some message -> ok $"received {message}" + | None -> ok "cancelled" }), source.Token) + |> ignore) + equal "cancelled" outcome + } + testCase "Simple async translates without exception" <| fun () -> async { return () } |> Async.StartImmediate diff --git a/tests/Js/Main/js/async-runtime.js b/tests/Js/Main/js/async-runtime.js new file mode 100644 index 0000000000..871ac9224d --- /dev/null +++ b/tests/Js/Main/js/async-runtime.js @@ -0,0 +1,36 @@ +// Controlled native fixtures; test cases and assertions live in AsyncTests.fs. +export function listenerCount(token) { + return token._listeners.size; +} + +export function cancellationErrorCount(error) { + if (error.constructor.name !== "CancellationCallbackError" || !Array.isArray(error.errors)) { + return -1; + } + return error.errors.length; +} + +export function withTimers(action) { + const originalSetTimeout = globalThis.setTimeout; + const originalClearTimeout = globalThis.clearTimeout; + let callback; + const timer = { + retired: 0, + invalidHandles: 0, + fire() { + // decision: fires retired callbacks to model a timeout already queued at cancellation + callback(); + }, + }; + globalThis.setTimeout = f => { callback = f; return 123; }; + globalThis.clearTimeout = id => { + if (id !== 123) { timer.invalidHandles++; } + timer.retired++; + }; + try { + return action(timer); + } finally { + globalThis.setTimeout = originalSetTimeout; + globalThis.clearTimeout = originalClearTimeout; + } +} diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index cb701222d1..3c8563ae9e 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -3,6 +3,35 @@ module Fable.Tests.Async open System open Util.Testing +#if FABLE_COMPILER +module private Runtime = + open Fable.Core.PyInterop + + type Observation = + abstract terminal: string array + abstract errors: string array + abstract before: string array + abstract outcome: string array + abstract listeners: int + abstract scheduled: int + abstract cancel_count: int + abstract retired: bool + abstract owner_loop: bool + abstract threads_finished: bool + abstract pending_before: bool + abstract pending_after: bool + + type ObserveSleep = delegate of Async * System.Threading.CancellationToken * string -> Observation + type ObserveMailbox = delegate of Async * MailboxProcessor * System.Threading.CancellationToken -> Observation + type ObserveOwnerLoop = delegate of Async * System.Threading.CancellationToken -> Observation + type ObserveSettlement = delegate of ((unit -> unit) -> Async) * System.Threading.CancellationToken * string -> Observation + + let observeSleep: ObserveSleep = import "observe_sleep" "./py/async_runtime.py" + let observeMailbox: ObserveMailbox = import "observe_mailbox" "./py/async_runtime.py" + let observeOwnerLoop: ObserveOwnerLoop = import "observe_owner_loop" "./py/async_runtime.py" + let observeSettlement: ObserveSettlement = import "observe_settlement" "./py/async_runtime.py" +#endif + type DisposableAction(f) = interface IDisposable with member _.Dispose() = f() @@ -706,3 +735,217 @@ let ``test unit continuations preserve captured callbacks`` () = namedUnitCapture 3 2 equal 5 calls +[] +let ``test cooperative cancellation stops at the next async boundary`` () = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let mutable terminal = "" + let mutable reached = false + let work = async { + try + source.Cancel() + do! async { return () } + reached <- true + finally + finalizers <- finalizers + 1 + } + Async.StartWithContinuations(work, (fun () -> terminal <- "success"), (fun _ -> terminal <- "error"), (fun _ -> terminal <- "cancel"), source.Token) + equal false reached + equal 1 finalizers + equal "cancel" terminal + + +[] +let ``test finalizer failure overrides success once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! successWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + +[] +let ``test finalizer failure overrides body failure once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! errorWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun error -> terminal.Add error.Message), (fun _ -> terminal.Add "cancel")) + equal [| "finalizer" |] (terminal.ToArray()) + equal 1 finalizers + +[] +let ``test finalizer failure preserves cancellation once`` () = + let mutable finalizers = 0 + let terminal = ResizeArray() + let work = async { + try return! cancelWork + finally + finalizers <- finalizers + 1 + failwith "finalizer" + } + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun _ -> terminal.Add "error"), (fun _ -> terminal.Add "cancel")) + equal [| "cancel" |] (terminal.ToArray()) + equal 1 finalizers + +#if FABLE_COMPILER +let private checkSleep cancelFirst throws = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let work = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + if throws then failwith "finalizer" + } + let result = Runtime.observeSleep.Invoke(work, source.Token, if cancelFirst then "cancel" else "timeout") + let expected = if cancelFirst then "cancel" elif throws then "error" else "success" + equal [| expected |] result.terminal + equal 1 finalizers + equal 0 result.listeners + equal 1 result.scheduled + equal true result.retired + equal 1 result.cancel_count + if throws && not cancelFirst then equal [| "finalizer" |] result.errors + else equal [||] result.errors + +[] +let ``test cancelled Sleep ignores stale timers and cleans up`` () = + checkSleep true false + +[] +let ``test completed Sleep ignores cancellation and stale timers`` () = + checkSleep false false + +[] +let ``test cancelled Sleep ignores a throwing finalizer and stale timers`` () = + checkSleep true true + +[] +let ``test precancelled Sleep does not execute or schedule`` () = + use source = new System.Threading.CancellationTokenSource() + let mutable reached = false + let work = async { + reached <- true + do! Async.Sleep 100 + } + let result = Runtime.observeSleep.Invoke(work, source.Token, "precancel") + equal [| "cancel" |] result.terminal + equal false reached + equal 0 result.scheduled + equal 0 result.listeners + +[] +let ``test idle Receive is cancelled by its token and keeps later posts queued`` () = + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let result = Runtime.observeMailbox.Invoke(mailbox.Receive(), mailbox, source.Token) + equal [| "cancel" |] result.before + equal false result.pending_before + equal [| "cancel" |] result.terminal + equal false result.pending_after + equal 0 result.listeners + equal [| "42" |] result.outcome + +[] +let ``test Sleep cancellation from another thread runs on the owner loop`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeOwnerLoop.Invoke(Async.Sleep 100000, source.Token) + equal [| "cancel" |] result.terminal + equal true result.owner_loop + equal true result.threads_finished + equal 0 result.listeners + +let private checkSettlement winner = + use source = new System.Threading.CancellationTokenSource() + let mutable finalizers = 0 + let makeWork timeout = async { + try do! Async.Sleep 100 + finally + finalizers <- finalizers + 1 + timeout () + } + let result = Runtime.observeSettlement.Invoke(makeWork, source.Token, winner) + equal [| winner |] result.outcome + equal [| if winner = "timeout" then "success" else "cancel" |] result.terminal + equal 1 finalizers + equal true result.threads_finished + equal 0 result.listeners + equal true result.retired + equal 1 result.cancel_count + +[] +let ``test reply wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "reply" + +[] +let ``test cancellation wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "cancel" + +[] +let ``test timeout wins concurrent reply cancellation and timeout settlement`` () = + checkSettlement "timeout" + +[] +let ``test cancellation during Sleep installation releases resources`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeSleep.Invoke(Async.Sleep 100, source.Token, "install_cancel") + equal [| "cancel" |] result.terminal + equal 0 result.listeners + equal 1 result.scheduled + equal true result.retired + equal 1 result.cancel_count + +[] +let ``test failed Sleep installation releases its registration`` () = + use source = new System.Threading.CancellationTokenSource() + let result = Runtime.observeSleep.Invoke(Async.Sleep 100, source.Token, "install_error") + equal [| "error" |] result.terminal + equal [| "scheduler" |] result.errors + equal 0 result.listeners + equal 0 result.scheduled +#endif + +[] +let ``test cancelling a waiting Receive reports cancel and keeps later posts queued`` () = + async { + use source = new System.Threading.CancellationTokenSource() + let mailbox = new MailboxProcessor((fun _ -> async { return () }), source.Token) + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + Async.StartWithContinuations(mailbox.Receive(), (fun value -> ok $"value {value}"), (fun _ -> ok "error"), (fun _ -> ok "cancel"), source.Token) + source.Cancel()) + equal "cancel" outcome + mailbox.Post 42 + let! next = mailbox.Receive() + equal 42 next + } |> Async.RunSynchronously + +[] +let ``test cancelling the mailbox token cancels a body waiting in Receive`` () = + async { + use source = new System.Threading.CancellationTokenSource() + let! outcome = + Async.FromContinuations(fun (ok, _, _) -> + MailboxProcessor.Start((fun inbox -> async { + let received = ref None + try + source.CancelAfter 50 + let! message = inbox.Receive() + received.Value <- Some message + finally + match received.Value with + | Some message -> ok $"received {message}" + | None -> ok "cancelled" }), source.Token) + |> ignore) + equal "cancelled" outcome + } |> Async.RunSynchronously diff --git a/tests/Python/py/async_runtime.py b/tests/Python/py/async_runtime.py new file mode 100644 index 0000000000..52dd18d55d --- /dev/null +++ b/tests/Python/py/async_runtime.py @@ -0,0 +1,248 @@ +"""Controlled scheduler and thread fixtures for the generated F# async tests.""" + +import asyncio +from collections.abc import Callable +from dataclasses import dataclass, field +from threading import Barrier, Event, Thread + +from fable_library.array_ import Array +from fable_library.async_builder import Async, CancellationToken, IAsyncContext, Trampoline +from fable_library.mailbox_processor import MailboxProcessor +from fable_library.util import lock + + +@dataclass +class Observation: + _terminal: list[str] = field(default_factory=list) + _errors: list[str] = field(default_factory=list) + _before: list[str] = field(default_factory=list) + _outcome: list[str] = field(default_factory=list) + listeners: int = 0 + scheduled: int = 0 + cancel_count: int = 0 + retired: bool = False + owner_loop: bool = False + threads_finished: bool = False + pending_before: bool = False + pending_after: bool = False + + @property + def terminal(self) -> Array[str]: + return Array(self._terminal) + + @property + def errors(self) -> Array[str]: + return Array(self._errors) + + @property + def before(self) -> Array[str]: + return Array(self._before) + + @property + def outcome(self) -> Array[str]: + return Array(self._outcome) + + +class Timer(asyncio.TimerHandle): + def __init__(self, callback: Callable[[], None]): + super().__init__(0, callback, (), asyncio.get_running_loop()) + self.callback = callback + self.retired = False + self.cancel_count = 0 + + def cancel(self): + self.retired = True + self.cancel_count += 1 + + def fire(self): + # decision: fires retired callbacks to model a timeout already queued when cancellation wins + self.callback() + + +class Scheduler(Trampoline): + def __init__(self, token: CancellationToken, mode: str = ""): + super().__init__() + self.token = token + self.mode = mode + self.timers: list[Timer] = [] + + def run_later(self, action: Callable[[], None], due_time: float = 0.0): + if self.mode == "install_error": + raise ValueError("scheduler") + timer = Timer(action) + self.timers.append(timer) + if self.mode == "install_cancel": + self.token.cancel() + return timer + + +def context(scheduler: Trampoline, token: CancellationToken, result: Observation) -> IAsyncContext[None]: + def failed(error: Exception): + result._terminal.append("error") + result._errors.append(str(error)) + + return IAsyncContext.create( + scheduler, + token, + lambda _: result._terminal.append("success"), + failed, + lambda _: result._terminal.append("cancel"), + ) + + +def observe_sleep(work: Async[None], token: CancellationToken, mode: str) -> Observation: + async def run(): + result = Observation() + scheduler = Scheduler(token, mode) + if mode == "precancel": + token.cancel() + work(context(scheduler, token, result)) + if scheduler.timers: + timer = scheduler.timers[0] + if mode == "cancel": + token.cancel() + timer.fire() + elif mode == "timeout": + timer.fire() + token.cancel() + timer.fire() + result.retired = timer.retired + result.cancel_count = timer.cancel_count + result.scheduled = len(scheduler.timers) + result.listeners = len(token.listeners) + return result + + return asyncio.run(run()) + + +def observe_mailbox(work: Async[int], mailbox: MailboxProcessor[int], token: CancellationToken) -> Observation: + async def run(): + result = Observation() + ctx = IAsyncContext.create( + Trampoline(), + token, + lambda value: result._terminal.append(str(value)), + lambda _: result._terminal.append("error"), + lambda _: result._terminal.append("cancel"), + ) + work(ctx) + token.cancel() + result._before = result._terminal.copy() + result.pending_before = mailbox.continuation is not None + mailbox.post(42) + result.pending_after = mailbox.continuation is not None + result.listeners = len(token.listeners) + next_ctx = IAsyncContext.create( + Trampoline(), + CancellationToken(), + lambda value: result._outcome.append(str(value)), + lambda _: result._outcome.append("error"), + lambda _: result._outcome.append("cancel"), + ) + mailbox.receive()(next_ctx) + return result + + return asyncio.run(run()) + + +def observe_owner_loop(work: Async[None], token: CancellationToken) -> Observation: + async def run(): + result = Observation() + loop = asyncio.get_running_loop() + finished = loop.create_future() + + def cancelled(_): + result.owner_loop = asyncio.get_running_loop() is loop + result._terminal.append("cancel") + finished.set_result(None) + + ctx = IAsyncContext.create( + Trampoline(), + token, + lambda _: result._terminal.append("success"), + lambda _: result._terminal.append("error"), + cancelled, + ) + work(ctx) + thread = Thread(target=token.cancel) + thread.start() + try: + await asyncio.wait_for(finished, 5) + finally: + thread.join(5) + result.threads_finished = not thread.is_alive() + result.listeners = len(token.listeners) + return result + + return asyncio.run(run()) + + +def observe_settlement( + make_work: Callable[[Callable[[], None]], Async[None]], + token: CancellationToken, + winner: str, +) -> Observation: + async def run(): + result = Observation() + scheduler = Scheduler(token) + key = object() + gate = Barrier(4) + winner_done = Event() + loop = asyncio.get_running_loop() + finished = loop.create_future() + thread_errors: list[Exception] = [] + + def choose(tag: str): + def settle(): + if result._outcome: + return + result._outcome.append(tag) + if tag == "reply": + # invariant: the winning reply retires its deadline before competitors proceed + token.cancel() + winner_done.set() + loop.call_soon_threadsafe(finished.set_result, None) + + lock(key, settle) + + registration = token.register(lambda: choose("cancel")) + make_work(lambda: choose("timeout"))(context(scheduler, token, result)) + timer = scheduler.timers[0] + + def compete(tag: str): + try: + gate.wait(5) + if tag != winner and not winner_done.wait(5): + raise TimeoutError("settlement winner did not signal") + if tag == "reply": + choose("reply") + elif tag == "cancel": + token.cancel() + else: + loop.call_soon_threadsafe(timer.fire) + except Exception as error: + thread_errors.append(error) + + threads = [Thread(target=compete, args=(tag,)) for tag in ("reply", "cancel", "timeout")] + for thread in threads: + thread.start() + gate.wait(5) + try: + await asyncio.wait_for(finished, 5) + finally: + for thread in threads: + thread.join(5) + registration.Dispose() + token.cancel() + if thread_errors: + raise ExceptionGroup("fixture worker failed", thread_errors) + # Exercise a stale callback after every winner, including cancellation's finalizer. + timer.fire() + await asyncio.sleep(0) + result.threads_finished = all(not thread.is_alive() for thread in threads) + result.listeners = len(token.listeners) + result.cancel_count = timer.cancel_count + result.retired = timer.retired + return result + + return asyncio.run(run()) diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py index aba116e32e..3016c43571 100644 --- a/tests/Python/test_async_runtime.py +++ b/tests/Python/test_async_runtime.py @@ -191,3 +191,28 @@ async def run() -> None: event.Trigger(42) assert calls == ["cancel"] assert not event.delegates + + +def test_sleep_cancelled_while_its_loop_is_stopped_settles_once(): + loop = asyncio.new_event_loop() + token = CancellationToken() + calls: list[str] = [] + + async def start() -> None: + start_with_continuations( + sleep(100_000), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + token, + ) + + try: + loop.run_until_complete(start()) + cancel_thread = Thread(target=token.cancel) + cancel_thread.start() + cancel_thread.join(5) + assert calls == ["cancel"] + assert not token.listeners + finally: + loop.close() From 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 12 Oct 2026 15:52:20 +0200 Subject: [PATCH 153/322] chore: upgrade xunit from v2 to v3 (#4623) Co-authored-by: Claude Opus 4.8 (1M context) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 7 +++++-- tests/Beam/Fable.Tests.Beam.fsproj | 14 +++++++++++--- tests/Beam/Main.fs | 6 ------ tests/Beam/xunit.runner.json | 5 +++++ tests/Php/Fable.Tests.Php.fsproj | 10 +++++++--- tests/Php/Main.fs | 6 ------ tests/Python/Fable.Tests.Python.fsproj | 14 +++++++++++--- tests/Python/Main.fs | 6 ------ tests/Python/xunit.runner.json | 5 +++++ tests/Rust/Fable.Tests.Rust.fsproj | 10 ++++++++-- tests/Rust/tests/src/lib.fs | 4 ---- 11 files changed, 52 insertions(+), 35 deletions(-) delete mode 100644 tests/Beam/Main.fs create mode 100644 tests/Beam/xunit.runner.json delete mode 100644 tests/Php/Main.fs delete mode 100644 tests/Python/Main.fs create mode 100644 tests/Python/xunit.runner.json diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 6c0b7943cf..f12cb78d84 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -6480,12 +6480,16 @@ module Compiler = ModuleDepth = 0 } + let entryPointItems = file.Declarations |> getEntryPointItems com ctx + let topAttrs = [ if isLastFileInProject com then // adds "no_std" to crate if feature is enabled mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_std" ] - mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_main" ] + // Libraries retain Cargo's test-harness entry point. + if not (List.isEmpty entryPointItems) then + mkInnerAttr "cfg_attr" [ "feature = \"no_std\""; "no_main" ] // TODO: make some of those conditional on compiler options mkInnerAttr "allow" [ "dead_code" ] @@ -6507,7 +6511,6 @@ module Compiler = // mkInnerAttr "feature" ["destructuring_assignment"] ] - let entryPointItems = file.Declarations |> getEntryPointItems com ctx let importItems = com.GetAllImports(ctx) |> transformImports com ctx let declItems = file.Declarations |> transformDeclarations com ctx let modItems = getModuleItems com ctx // global module imports diff --git a/tests/Beam/Fable.Tests.Beam.fsproj b/tests/Beam/Fable.Tests.Beam.fsproj index 54bd8eb912..f310f1716d 100644 --- a/tests/Beam/Fable.Tests.Beam.fsproj +++ b/tests/Beam/Fable.Tests.Beam.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -10,8 +14,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,6 +29,10 @@ + + + + @@ -104,6 +113,5 @@ - diff --git a/tests/Beam/Main.fs b/tests/Beam/Main.fs deleted file mode 100644 index b00af3eaac..0000000000 --- a/tests/Beam/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif diff --git a/tests/Beam/xunit.runner.json b/tests/Beam/xunit.runner.json new file mode 100644 index 0000000000..8641732346 --- /dev/null +++ b/tests/Beam/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} diff --git a/tests/Php/Fable.Tests.Php.fsproj b/tests/Php/Fable.Tests.Php.fsproj index 509b2813e4..b670ce39b8 100644 --- a/tests/Php/Fable.Tests.Php.fsproj +++ b/tests/Php/Fable.Tests.Php.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -9,8 +13,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -31,7 +36,6 @@ - diff --git a/tests/Php/Main.fs b/tests/Php/Main.fs deleted file mode 100644 index ae5f6f6959..0000000000 --- a/tests/Php/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif \ No newline at end of file diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 24ef0fbcf9..92bfcf5106 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -1,6 +1,10 @@ Exe + + false + false + false net10.0 Major false @@ -10,8 +14,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -22,6 +27,10 @@ + + + + @@ -88,6 +97,5 @@ - diff --git a/tests/Python/Main.fs b/tests/Python/Main.fs deleted file mode 100644 index ae5f6f6959..0000000000 --- a/tests/Python/Main.fs +++ /dev/null @@ -1,6 +0,0 @@ -#if FABLE_COMPILER -module Program -() -#else -module Program = let [] main _ = 0 -#endif \ No newline at end of file diff --git a/tests/Python/xunit.runner.json b/tests/Python/xunit.runner.json new file mode 100644 index 0000000000..8641732346 --- /dev/null +++ b/tests/Python/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index 2b9a48b8ba..dfd8096cf8 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -1,5 +1,10 @@ + Exe + + false + false + false net10.0 Major false @@ -8,8 +13,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Rust/tests/src/lib.fs b/tests/Rust/tests/src/lib.fs index e448cd8f1f..dd4a08ea7a 100644 --- a/tests/Rust/tests/src/lib.fs +++ b/tests/Rust/tests/src/lib.fs @@ -5,7 +5,3 @@ open Fable.Core.Rust let _imports() = importAll "./ExtInteropTests.rs" () - -// Comment the main func when no longer needed (after migrating to xunit v3) -[] -let main _args = 0 From ee4dd7c0366a626df0447c32f28cef9a72410983 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:31:06 +0200 Subject: [PATCH 154/322] chore: release multiple projects (#5050) --- src/Fable.Cli/CHANGELOG.md | 17 ++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 17 ++++++++++++++++- src/Fable.Core/CHANGELOG.md | 10 +++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 20 +++++++++++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 11 ++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 17 ++++++++++++++++- src/fable-standalone/package.json | 4 ++-- 10 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index decd31de69..a1f83e8395 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.20.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 5.20.0 - 2026-10-07 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index e9e0f73271..36dafa9ebf 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.19.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 5.19.0 - 2026-10-07 ### 🚀 Features diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index 089f37dd0d..d4886e19f5 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.4.0 - 2026-10-12 + +### 🚀 Features + +* *(all)* Add implicit conversion to erased unions U2-U9 ([4e486f20](https://github.com/fable-compiler/Fable/commit/4e486f201fb229964af213d6ce3030ac3c7e26de)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 5.3.0 - 2026-09-25 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index ee3e965925..1975606335 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.20.0" + let VERSION = "5.20.1" [] - let JS_LIBRARY_VERSION = "2.8.0" + let JS_LIBRARY_VERSION = "2.8.1" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 53187bcdee..9f621a87b6 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.8.0 - 2026-10-12 + +### 🚀 Features + +* *(all)* Add implicit conversion to erased unions U2-U9 ([4e486f20](https://github.com/fable-compiler/Fable/commit/4e486f201fb229964af213d6ce3030ac3c7e26de)) + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 3.7.0 - 2026-10-07 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 46c0be42a0..3b43d76d9b 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.7.0", + "version": "3.8.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.7.0" + "@fable-org/fable-standalone": "^3.7.1" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index eef1a37bc5..9c1fda5f0f 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2369fbd24fafe851544c1ed6eb54f0bfc2b75f36 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 updaters: - package.json: file: package.json @@ -15,6 +15,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.8.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2369fbd24fafe851544c1ed6eb54f0bfc2b75f36..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 2.8.0 - 2026-09-25 ### 🐞 Bug Fixes diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index bbc2f13cea..870c0870a2 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.8.0", + "version": "2.8.1", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 0ada496219..a43a52ab4a 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 23bc5c54db51469431e6fdab713dfe1b6a3df019 +last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.7.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(beam)* Disambiguate property accessors from ordinary methods (#5049) ([b30dc89d](https://github.com/fable-compiler/Fable/commit/b30dc89d0091c2bd1d9fd7a20042b7fbb4e5a3fa)) +* *(beam)* Normalize exception messages and string split separators (#5053) ([f13a9e14](https://github.com/fable-compiler/Fable/commit/f13a9e1494d8bbde8e74bfbf9dfbc8bd051a157e)) +* *(js/ts/python)* Synchronize cancellation registrations (#5036) ([07db576c](https://github.com/fable-compiler/Fable/commit/07db576c4ae16dcef9d1773ac4d6c70b2a223f0a)) +* *(js/ts/python)* Preserve JS/TS generic unit arguments and Python unit continuations (#5037) ([e05bc752](https://github.com/fable-compiler/Fable/commit/e05bc752f1d8f16f08e2409a191d9c33463a1f04)) +* *(js/ts/python)* Settle cancelled delays and finalizers once (#5038) ([a9802484](https://github.com/fable-compiler/Fable/commit/a9802484c8cdeadd5203123d76f42e375c1c3159)) +* *(rust)* Fix tailcall elimination for members (#5051) ([4eb21772](https://github.com/fable-compiler/Fable/commit/4eb2177263f544ec354076da07d8a097d56c273e)) +* *(rust)* Fix temporary struct locals type (#5052) ([17b36588](https://github.com/fable-compiler/Fable/commit/17b3658876a3bd776c4a0129ec3a4cddc9d7e05d)) +* *(rust)* Add main function for no_std (#5054) ([10fdccc0](https://github.com/fable-compiler/Fable/commit/10fdccc04bb0e421643426d21dfcbd940e3e5a16)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/23bc5c54db51469431e6fdab713dfe1b6a3df019..148f989e1b37b3f6560c3dda615b6a3a8efffcb8) + ## 3.7.0 - 2026-10-07 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 787033ae5e..8ef9f03e63 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.7.0", - "fableVersion": "5.20.0", + "version": "3.7.1", + "fableVersion": "5.20.1", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 7eac4ec30b97f8890e7fea830322286e9483b066 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 12 Oct 2026 21:04:59 +0200 Subject: [PATCH 155/322] ci: test Python 3.13 through 3.15 (#5056) --- .github/workflows/build.yml | 4 ++-- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/python-type-checking.yml | 4 ++-- AGENTS.md | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c6b4933675..3e2920024f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,7 +137,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, windows-latest] - python-version: [3.12, 3.13, 3.14] + python-version: ["3.13", "3.14", "3.15"] runs-on: ${{ matrix.platform }} env: UV_LINK_MODE: copy @@ -197,7 +197,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.12", "3.13", "3.14"] + python-version: ["3.13", "3.14", "3.15"] runs-on: ${{ matrix.platform }} env: UV_LINK_MODE: copy diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 546bfa670a..178d5f3ee7 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v7 with: - python-version: '3.12' + python-version: '3.13' - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Install uv diff --git a/.github/workflows/python-type-checking.yml b/.github/workflows/python-type-checking.yml index 5b2e67cde2..f524ab542c 100644 --- a/.github/workflows/python-type-checking.yml +++ b/.github/workflows/python-type-checking.yml @@ -31,10 +31,10 @@ jobs: - name: Setup dotnet tools run: dotnet tool restore - - name: Set up Python 3.12 + - name: Set up Python 3.13 uses: actions/setup-python@v7 with: - python-version: "3.12" + python-version: "3.13" - name: Install uv run: | diff --git a/AGENTS.md b/AGENTS.md index acc6bdd173..53a4b0ac4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,7 +98,7 @@ Be careful when modifying shared files like `FableTransforms.fs`, `Transforms.Ut `src/fable-library-py/` is distributed on PyPI and must be backward-compatible within minor versions. Do not change the semantics of existing functions — instead, create a new function with a new name and update `Replacements.fs` to call it. -All code in `fable-library-py` must be fully type-annotated using Python 3.12+ syntax (PEP 695). Avoid `Any`, `cast()`, and `# type: ignore` — these require justification. Keep functions small and simple: extract helpers and use early returns. The Rust extension modules (`fable-library-core`) exist to provide correct .NET semantics for numerics, arrays, etc. — not for performance (the FFI crossing is expensive). +All code in `fable-library-py` must be fully type-annotated using Python 3.13-compatible syntax, including PEP 695 type parameters. Avoid `Any`, `cast()`, and `# type: ignore` — these require justification. Keep functions small and simple: extract helpers and use early returns. The Rust extension modules (`fable-library-core`) exist to provide correct .NET semantics for numerics, arrays, etc. — not for performance (the FFI crossing is expensive). All transpiled Python code is type-checked with Pyright at standard settings (`./build.sh test python --type-check`). Do not introduce type-checking regressions without justification. @@ -125,7 +125,7 @@ DO NOT update the changelog files. These files are automatically generated based - .NET SDK 10+ - Node.js with npm -- Python 3.12+ with [uv](https://docs.astral.sh/uv/) (3.12+ required for PEP 695 type parameter syntax) +- Python 3.13+ with [uv](https://docs.astral.sh/uv/) (CI tests 3.13, 3.14, and 3.15; type checking and publish preparation use 3.13) - Rust toolchain (for Rust target) - Dart SDK (for Dart target) - Erlang/OTP with rebar3 (for BEAM target) From 617f7111bf02f8536b321b4373bdffa7a0f7d633 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Mon, 12 Oct 2026 22:24:10 +0200 Subject: [PATCH 156/322] fix(beam): preserve async cancellation and finalize resources once (#5057) --- src/Fable.Transforms/Beam/Fable2Beam.fs | 69 +---- src/Fable.Transforms/Beam/Replacements.fs | 12 + src/fable-library-beam/fable_async.erl | 81 +++++- .../fable_async_builder.erl | 59 +++- src/fable-library-beam/fable_cancellation.erl | 10 +- tests/Beam/AsyncTests.fs | 270 +++++++++++++++++- tests/Beam/erl/async_lifecycle_tests.erl | 264 +++++++++++++++++ tests/Beam/erl/async_stacktrace_tests.erl | 33 ++- 8 files changed, 699 insertions(+), 99 deletions(-) create mode 100644 tests/Beam/erl/async_lifecycle_tests.erl diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 058dc29f7b..9ee0cf6453 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -982,75 +982,12 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er if catchBodyExprs |> List.exists (referencesVariable identVar) then let reasonRef = Beam.ErlExpr.Variable reasonVar - let formatExpr = - Beam.ErlExpr.Call( - None, - "iolist_to_binary", - [ - Beam.ErlExpr.Call( - Some "io_lib", - "format", - [ - Beam.ErlExpr.Call( - None, - "binary_to_list", - [ Beam.ErlExpr.Literal(Beam.ErlLiteral.StringLit "~p") ] - ) - Beam.ErlExpr.List [ reasonRef ] - ] - ) - ] - ) - - let messageExpr = - Beam.ErlExpr.Case( - reasonRef, - [ - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_binary", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [] - Body = [ formatExpr ] - } - ] - ) - - // If reason is already a map (F# exception) or reference (class inheriting exn), use it directly. - // Otherwise wrap in #{message => ...} for .Message access. + // invariant: Ordinary catches retain nominal cancellation identity and remain allowed to handle it. + // Use the same normalization as Async.TryWith; reraise still uses the raw catch variables. let bindIdent = Beam.ErlExpr.Match( Beam.PVar identVar, - Beam.ErlExpr.Case( - reasonRef, - [ - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_map", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [ Beam.ErlExpr.Call(None, "is_reference", [ reasonRef ]) ] - Body = [ reasonRef ] - } - { - Pattern = Beam.PWildcard - Guard = [] - Body = - [ - Beam.ErlExpr.Map - [ - Beam.ErlExpr.Literal(Beam.ErlLiteral.AtomLit(Beam.Atom "message")), - messageExpr - ] - ] - } - ] - ) + Beam.ErlExpr.Call(Some "fable_async", "wrap_error", [ reasonRef ]) ) Beam.ErlExpr.TryCatch( diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 8408d3b4d8..ff1eedd1fc 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -3332,6 +3332,18 @@ let asyncs (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (_thisArg: Expr op | "Catch" -> Helper.LibCall(com, "fable_async", "catch_async", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "RunSynchronously" -> + // decision: Keep timeout and token in separate slots; the legacy /2 runtime also accepts a token. + let omitted = Null Any |> makeValue r + + let args = + match args with + | [ computation ] -> [ computation; omitted; omitted ] + | [ computation; timeout ] -> [ computation; timeout; omitted ] + | _ -> args + + Helper.LibCall(com, "fable_async", "run_synchronously", t, args, ?loc = r) + |> Some | meth -> Helper.LibCall(com, "fable_async", Naming.lowerFirst meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some diff --git a/src/fable-library-beam/fable_async.erl b/src/fable-library-beam/fable_async.erl index 0c2fe5bb16..96ca37d44b 100644 --- a/src/fable-library-beam/fable_async.erl +++ b/src/fable-library-beam/fable_async.erl @@ -2,7 +2,7 @@ -export([ start_immediate/1, start_immediate/2, start_child/1, start_child/2, - run_synchronously/1, run_synchronously/2, + run_synchronously/1, run_synchronously/2, run_synchronously/3, start_with_continuations/4, start_with_continuations/5, sleep/1, await_event/1, await_event/2, @@ -34,7 +34,8 @@ -spec start_child(async(term())) -> async(async(term())). -spec start_child(async(term()), non_neg_integer() | undefined) -> async(async(term())). -spec run_synchronously(async(term())) -> term(). --spec run_synchronously(async(term()), reference() | undefined) -> term(). +-spec run_synchronously(async(term()), integer() | reference() | undefined) -> term(). +-spec run_synchronously(async(term()), integer() | undefined, reference() | undefined) -> term(). -spec start_with_continuations(async(term()), fun(), fun(), fun()) -> term(). -spec start_with_continuations(async(term()), fun(), fun(), fun(), reference() | undefined) -> term(). @@ -172,9 +173,53 @@ child_result_holder(ResultRef, Result) -> child_result_holder(ResultRef, Result) end. -%% RunSynchronously: run CPS chain in current process, capture result -run_synchronously(Computation) -> run_synchronously(Computation, undefined). +%% RunSynchronously: unlimited waits run inline; finite deadlines use a worker. +run_synchronously(Computation) -> run_synchronously(Computation, undefined, undefined). +%% decision: retain native /2 token callers while compiled F# uses the unambiguous /3 ABI. +run_synchronously(Computation, Timeout) when is_integer(Timeout) -> + run_synchronously(Computation, Timeout, undefined); run_synchronously(Computation, CancelToken) -> + run_synchronously(Computation, undefined, CancelToken). + +run_synchronously(Computation, Timeout, CancelToken) when Timeout =:= undefined; Timeout =:= -1 -> + run_inline(Computation, CancelToken); +run_synchronously(Computation, Timeout, CancelToken) when is_integer(Timeout), Timeout >= 0 -> + run_with_timeout(Computation, Timeout, CancelToken); +run_synchronously(_Computation, _Timeout, _CancelToken) -> + erlang:error(#{exn_type => argument_exception, message => <<"The timeout must be non-negative or -1.">>}). + +%% decision: finite timeouts use a monitored worker so blocking BEAM operations cannot block the deadline. +%% invariant: timeout requests cooperative cancellation rather than killing work and skipping finalizers. +%% tradeoff: timed work runs in another process and cannot share process-dictionary-backed mutable values. +run_with_timeout(Computation, Timeout, CancelToken) -> + Token = fable_cancellation:create(), + Registration = fable_cancellation:register(CancelToken, fun(_) -> fable_cancellation:cancel(Token) end), + {Pid, Monitor} = spawn_monitor(fun() -> + Result = try run_inline(Computation, CancelToken, Token) of + Value -> {ok, Value} + catch Class:Reason:Stacktrace -> {error, Class, Reason, Stacktrace} + end, + exit({fable_async_result, Result}) + end), + try + receive + {'DOWN', Monitor, process, Pid, {fable_async_result, {ok, Value}}} -> Value; + {'DOWN', Monitor, process, Pid, {fable_async_result, {error, Class, Reason, Stacktrace}}} -> + erlang:raise(Class, Reason, Stacktrace); + {'DOWN', Monitor, process, Pid, Reason} -> erlang:error(Reason) + after Timeout -> + fable_cancellation:cancel(Token), + erlang:error(#{exn_type => timeout_exception, message => <<"The operation has timed out.">>}) + end + after + erlang:demonitor(Monitor, [flush]), + fable_utils:safe_dispose(Registration) + end. + +run_inline(Computation, CancelToken) -> + run_inline(Computation, CancelToken, CancelToken). + +run_inline(Computation, UserToken, CancelToken) -> %% Use a unique ref as key to store result in process dict Ref = make_ref(), %% Take over stacktrace parking for the duration of this run, handing it back afterwards so a @@ -185,8 +230,11 @@ run_synchronously(Computation, CancelToken) -> Ctx = #{ on_success => fun(V) -> put(Ref, {ok, V}) end, on_error => fun(E) -> put(Ref, {error, E}) end, - on_cancel => fun(_) -> put(Ref, {cancelled}) end, + on_cancel => fun(E) -> put(Ref, {cancelled, E}) end, cancel_token => CancelToken, + %% invariant: Async.CancellationToken returns the supplied token even when a timeout + %% needs a linked token internally to cancel waits without cancelling the user's source. + user_cancel_token => UserToken, %% invariant: runtime-owned callbacks are handled by the process that created their %% process-dictionary-backed continuations. wait_for_async => true @@ -208,7 +256,14 @@ run_synchronously(Computation, CancelToken) -> case Result of {ok, Value} -> Value; {error, Error} -> reraise(Ref, Parked, Error); - {cancelled} -> erlang:error(operation_cancelled); + {cancelled, Error} -> + %% invariant: RunSynchronously rethrows the cancellation exception supplied by the + %% continuation; legacy native callbacks that pass unit still receive a typed exception. + Exception = wrap_error(Error), + case fable_utils:is_exception_type(Exception, operation_canceled_exception) of + true -> erlang:error(Exception); + false -> erlang:error(fable_cancellation:exception()) + end; undefined -> erlang:error(async_no_result) end. @@ -243,7 +298,7 @@ sleep(Milliseconds) -> %% Check if already cancelled before sleeping case fable_cancellation:is_cancellation_requested(Token) of true -> - (maps:get(on_cancel, Ctx))(ok); + (maps:get(on_cancel, Ctx))(fable_cancellation:exception()); false -> %% Set up a timer to send us a wake-up message Self = self(), @@ -258,7 +313,7 @@ sleep(Milliseconds) -> {sleep_done, TimerRef} -> (maps:get(on_success, Ctx))(ok); {sleep_cancelled, TimerRef} -> - (maps:get(on_cancel, Ctx))(ok) + (maps:get(on_cancel, Ctx))(fable_cancellation:exception()) end, erlang:cancel_timer(SleepTimer), fable_utils:safe_dispose(RegId) @@ -357,7 +412,7 @@ invoke_await_event_cancel(CancelAction, OnCancel) -> undefined -> ok; _ -> CancelAction(ok) end, - OnCancel(ok). + OnCancel(fable_cancellation:exception()). %% Parallel: spawn one process per computation, collect results in order parallel(Computations) when is_reference(Computations) -> @@ -411,7 +466,7 @@ sequential(Computations) -> %% Uses atom tags matching Beam union representation: choice1_of2 = Choice1Of2, choice2_of2 = Choice2Of2 catch_async(Computation) -> fun(Ctx) -> - Ctx2 = #{ + Ctx2 = Ctx#{ on_success => fun(V) -> (maps:get(on_success, Ctx))({choice1_of2, V}) end, on_error => fun(E) -> (maps:get(on_success, Ctx))({choice2_of2, wrap_error(E)}) end, on_cancel => maps:get(on_cancel, Ctx), @@ -443,10 +498,10 @@ start_as_task(Computation) -> %% convert — the task *is* the async computation. await_task(Task) -> Task. -%% CancellationToken: returns async that extracts cancel_token from context +%% CancellationToken: expose the supplied token independently of timeout wake-ups. cancellation_token() -> fun(Ctx) -> - Token = maps:get(cancel_token, Ctx), + Token = maps:get(user_cancel_token, Ctx, maps:get(cancel_token, Ctx)), (maps:get(on_success, Ctx))(Token) end. @@ -456,6 +511,8 @@ create_cancellation_token(Arg) -> fable_cancellation:create(Arg). %% Wrap raw error values as exception maps so .Message accessor works. %% Errors from failwith are raw binaries; errors from raise(exn ...) are already maps. +%% decision: recognize the legacy runtime atom by identity, never by a formatted message or token state. +wrap_error(operation_cancelled) -> fable_cancellation:exception(); wrap_error(Err) when is_binary(Err) -> #{message => Err}; wrap_error(Err) when is_map(Err) -> Err; wrap_error(Err) when is_reference(Err) -> Err; diff --git a/src/fable-library-beam/fable_async_builder.erl b/src/fable-library-beam/fable_async_builder.erl index 88a2e697d1..3ba2a5a27b 100644 --- a/src/fable-library-beam/fable_async_builder.erl +++ b/src/fable-library-beam/fable_async_builder.erl @@ -58,15 +58,22 @@ zero(_Unit) -> return(ok). %% Delay: defer generator execution (cold semantics) delay(Generator) -> - fun(Ctx) -> (Generator(ok))(Ctx) end. + fun(Ctx) -> + case fable_cancellation:is_cancellation_requested(maps:get(cancel_token, Ctx)) of + true -> (maps:get(on_cancel, Ctx))(fable_cancellation:exception()); + false -> (Generator(ok))(Ctx) + end + end. %% Bind: monadic bind — run computation, pass result to binder bind(Computation, Binder) -> fun(Ctx) -> Ctx2 = Ctx#{ on_success => fun(X) -> - try - (Binder(X))(Ctx) + %% Only binder evaluation is guarded. Exceptions from the returned computation + %% may belong to downstream continuations whose handlers have already settled. + try Binder(X) of + Bound -> Bound(Ctx) catch _:Err:St -> fable_async:record_stacktrace(Err, St), @@ -83,6 +90,8 @@ combine(Comp1, Comp2) -> %% TryWith: catch errors in computation, pass to handler %% Wrap raw errors (binaries from failwith) as exception maps so .Message works. +%% invariant: only on_error enters the handler; on_cancel bypasses it, while a thrown +%% OperationCanceledException remains catchable just like any explicitly thrown .NET exception. try_with(Computation, Handler) -> fun(Ctx) -> Ctx2 = Ctx#{ @@ -122,33 +131,53 @@ try_with(Computation, Handler) -> %% Compensation is generated as fun(UnitVar) -> ... end (arity 1), so call with ok. try_finally(Computation, Compensation) -> fun(Ctx) -> + %% invariant: settlement claims compensation before invoking it, so its exception cannot re-enter it. + %% decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally. + Settled = atomics:new(1, []), + Finish = fun(Channel, Value) -> + case atomics:compare_exchange(Settled, 1, 0, 1) of + ok -> + Result = try Compensation(ok), ok + catch + _:_ when Channel =:= on_cancel -> ok; + _:Err:St -> + fable_async:record_stacktrace(Err, St), + {error, Err} + end, + case Result of + ok -> (maps:get(Channel, Ctx))(Value); + {error, Error} -> (maps:get(on_error, Ctx))(Error) + end; + 1 -> ok + end + end, Ctx2 = Ctx#{ on_success => fun(X) -> - Compensation(ok), - (maps:get(on_success, Ctx))(X) + Finish(on_success, X) end, on_error => fun(E) -> - Compensation(ok), - (maps:get(on_error, Ctx))(E) + Finish(on_error, E) end, on_cancel => fun(E) -> - Compensation(ok), - (maps:get(on_cancel, Ctx))(E) + Finish(on_cancel, E) end }, try Computation(Ctx2) catch - _:Err:St -> - fable_async:record_stacktrace(Err, St), - Compensation(ok), - (maps:get(on_error, Ctx))(Err) + Class:Err:St -> + case atomics:get(Settled, 1) of + 0 -> + fable_async:record_stacktrace(Err, St), + Finish(on_error, Err); + 1 -> erlang:raise(Class, Err, St) + end end end. -%% Using: TryFinally with IDisposable (Dispose = no-op in Erlang for now) +%% Using: install disposal before evaluating the binder, including a binder that throws. using(Resource, Binder) -> - try_finally(Binder(Resource), fun() -> ok end). + try_finally(delay(fun(_) -> Binder(Resource) end), fun(_) -> fable_utils:safe_dispose(Resource) end). %% While: recursive bind while guard is true while(Guard, Computation) -> diff --git a/src/fable-library-beam/fable_cancellation.erl b/src/fable-library-beam/fable_cancellation.erl index beab9ee665..3f81037e7e 100644 --- a/src/fable-library-beam/fable_cancellation.erl +++ b/src/fable-library-beam/fable_cancellation.erl @@ -5,7 +5,8 @@ cancel_after/2, is_cancellation_requested/1, throw_if_cancellation_requested/1, - register/2, register/3 + register/2, register/3, + exception/0 ]). -spec create() -> reference(). @@ -14,6 +15,7 @@ -spec cancel_after(reference() | undefined, non_neg_integer()) -> ok. -spec is_cancellation_requested(reference() | undefined) -> boolean(). -spec throw_if_cancellation_requested(reference() | undefined) -> ok. +-spec exception() -> #{exn_type := operation_canceled_exception, message := binary()}. -spec register(reference() | undefined, fun()) -> map() | undefined. -spec register(reference() | undefined, fun(), term()) -> map() | undefined. @@ -72,10 +74,14 @@ throw_if_cancellation_requested(undefined) -> ok; throw_if_cancellation_requested(Token) -> case is_cancellation_requested(Token) of - true -> erlang:error(operation_cancelled); + true -> erlang:error(exception()); false -> ok end. +%% invariant: cancellation has a nominal exception tag, never just a matching message. +exception() -> + #{exn_type => operation_canceled_exception, message => <<"The operation was canceled.">>}. + register(Token, F) -> register(Token, F, ok). register(undefined, _F, _State) -> diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index c2011b6662..b318fb914e 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -3,6 +3,225 @@ module Fable.Tests.AsyncTests open System open Fable.Tests.Util open Util.Testing +open System.Threading + +let private raiseCaughtCancellation (error: exn) : unit = raise error + +[] +let ``test RunSynchronously optional timeout and cancellation token`` () = + let work = async { return 42 } + Async.RunSynchronously work |> equal 42 + Async.RunSynchronously(work, timeout = -1) |> equal 42 + Async.RunSynchronously(work, timeout = 10000) |> equal 42 + let invalidTimeout = + try + Async.RunSynchronously(work, timeout = -2) |> ignore + false + with :? ArgumentException -> true + equal true invalidTimeout + use cts = new CancellationTokenSource() + Async.RunSynchronously(work, cancellationToken = cts.Token) |> equal 42 + Async.RunSynchronously(work, timeout = 10000, cancellationToken = cts.Token) |> equal 42 + cts.Cancel() + let mutable entered = false + let cancelled = + try + Async.RunSynchronously(async { entered <- true }, cancellationToken = cts.Token) + false + with :? OperationCanceledException -> true + equal true cancelled + equal false entered + +[] +let ``test cancellation identity survives ordinary catch and raise`` () = + let token = CancellationToken(true) + let mutable caught = false + let cancelled = + try + try token.ThrowIfCancellationRequested() + with error -> + caught <- true + raiseCaughtCancellation error + false + with :? OperationCanceledException -> true + equal true caught + equal true cancelled + let ordinary = + try raise (Exception("operation_cancelled")) + with + | :? OperationCanceledException -> false + | error -> error.Message = "operation_cancelled" + equal true ordinary + +[] +let ``test Async TryWith catches explicitly thrown cancellation exceptions`` () = + async { + try + CancellationToken(true).ThrowIfCancellationRequested() + return false + with :? OperationCanceledException -> return true + } + |> Async.RunSynchronously + |> equal true + +[] +let ``test a real Async defect racing cancellation remains a failure`` () = + use cts = new CancellationTokenSource() + let work = async { + cts.Cancel() + return failwith "real defect" + } + let outcome = + try + Async.RunSynchronously(work, cancellationToken = cts.Token) |> ignore + "success" + with + | :? OperationCanceledException -> "cancelled" + | error -> error.Message + equal "real defect" outcome + +[] +let ``test Async finalizers run once and retain throwing compensation`` () = + for bodyFails in [ false; true ] do + let mutable count = 0 + let work = async { + try + if bodyFails then failwith "body defect" + return 42 + finally + count <- count + 1 + failwith "cleanup defect" + } + let failure = + try Async.RunSynchronously work |> ignore; "success" + with error -> error.Message + equal "cleanup defect" failure + equal 1 count + +[] +let ``test StartImmediate propagates errors after a finalizer containing a bind`` () = + let mutable count = 0 + let work = async { + try + let! value = async { return 42 } + equal 42 value + finally + count <- count + 1 + failwith "downstream defect" + } + let failure = + try Async.StartImmediate work; "success" + with error -> error.Message + equal "downstream defect" failure + equal 1 count + +[] +let ``test nested Async finalizers retain outer failure and run once`` () = + let mutable inner = 0 + let mutable outer = 0 + let work = async { + try + try return 42 + finally + inner <- inner + 1 + failwith "inner cleanup" + finally + outer <- outer + 1 + failwith "outer cleanup" + } + let failure = + try Async.RunSynchronously work |> ignore; "success" + with error -> error.Message + equal "outer cleanup" failure + equal 1 inner + equal 1 outer + +[] +let ``test Async Using disposes once and retains disposal defects`` () = + for bodyFails in [ false; true ] do + for disposalFails in [ false; true ] do + let mutable count = 0 + let resource = + { new IDisposable with + member _.Dispose() = + count <- count + 1 + if disposalFails then failwith "disposal defect" } + let work = async { + use owned = resource + if bodyFails then failwith "body defect" + return 42 + } + let outcome = + try Async.RunSynchronously work |> string + with error -> error.Message + equal (if disposalFails then "disposal defect" elif bodyFails then "body defect" else "42") outcome + equal 1 count + async { + use resource = Unchecked.defaultof + return 42 + } + |> Async.RunSynchronously + |> equal 42 + +let private cancelledComputation : Async = + Async.FromContinuations(fun (_, _, cancel) -> + cancel (OperationCanceledException("original cancellation"))) + +let private cancellationOutcome (work: Async) = + try + Async.RunSynchronously work + "success" + with + | :? OperationCanceledException as error -> error.Message + | error -> "failure: " + error.Message + +[] +let ``test Async cancellation remains terminal when compensation throws`` () = + for throws in [ false; true ] do + let mutable count = 0 + let work = async { + try do! cancelledComputation + finally + count <- count + 1 + if throws then failwith "cleanup defect" + } + equal "original cancellation" (cancellationOutcome work) + equal 1 count + +[] +let ``test nested throwing Async finalizers preserve cancellation`` () = + for outerThrows in [ false; true ] do + let mutable inner = 0 + let mutable outer = 0 + let work = async { + try + try do! cancelledComputation + finally + inner <- inner + 1 + failwith "inner cleanup" + finally + outer <- outer + 1 + if outerThrows then failwith "outer cleanup" + } + equal "original cancellation" (cancellationOutcome work) + equal 1 inner + equal 1 outer + +[] +let ``test Async Using preserves cancellation when Dispose throws`` () = + for throws in [ false; true ] do + let mutable count = 0 + let resource = + { new IDisposable with + member _.Dispose() = + count <- count + 1 + if throws then failwith "disposal defect" } + let work = async { + use owned = resource + do! cancelledComputation + } + equal "original cancellation" (cancellationOutcome work) + equal 1 count type MyException(value) = inherit Exception() @@ -444,9 +663,54 @@ let private runInCancellationProcess (f: unit -> 'a) : 'a = Fable.Core.Util.nati [ CancellationFun = $0, spawn(fun() -> case erlang:fun_info(CancellationFun, arity) of {arity, 0} -> CancellationFun(); _ -> CancellationFun(ok) end end) end)()")>] let private startCancellationProcess (f: unit -> unit) : obj = Fable.Core.Util.nativeOnly -[] -let private runSynchronouslyWithCancellation (_workflow: Async<'a>) (_token: System.Threading.CancellationToken) : 'a = - Fable.Core.Util.nativeOnly +let private runSynchronouslyWithCancellation (workflow: Async<'a>) (token: System.Threading.CancellationToken) : 'a = + Async.RunSynchronously(workflow, cancellationToken = token) + +[] +let private pendingCancellation (_parent: obj) : Async = Fable.Core.Util.nativeOnly + +[] +let ``test legacy runtime cancellation remains typed through normalization and raise`` () = + let cancelled = + try + try emitErlExpr () "erlang:error(operation_cancelled)" + with error -> raiseCaughtCancellation error + false + with :? OperationCanceledException -> true + equal true cancelled + +[] +let ``test pending RunSynchronously cancellation bypasses Async TryWith and disposes once`` () = + use cts = new CancellationTokenSource() + let parent: obj = emitErlExpr () "erlang:self()" + let work = async { + let! child = Async.StartChild(async { + let mutable handled = false + let mutable finalized = 0 + let mutable disposed = 0 + let resource = + { new IDisposable with + member _.Dispose() = disposed <- disposed + 1 } + let cancelled = + try + Async.RunSynchronously( + async { + use owned = resource + try + try do! pendingCancellation parent + with _ -> handled <- true + finally finalized <- finalized + 1 + }, cancellationToken = cts.Token) + false + with :? OperationCanceledException -> true + return cancelled, handled, finalized, disposed + }) + let ready: bool = emitErlExpr () "receive {pending, _} -> true after 5000 -> false end" + equal true ready + cts.Cancel() + return! child + } + Async.RunSynchronously work |> equal (true, false, 1, 1) let private sendCancellationReady (parent: obj) = let worker: obj = emitErlExpr () "erlang:self()" diff --git a/tests/Beam/erl/async_lifecycle_tests.erl b/tests/Beam/erl/async_lifecycle_tests.erl new file mode 100644 index 0000000000..0c6e8c748a --- /dev/null +++ b/tests/Beam/erl/async_lifecycle_tests.erl @@ -0,0 +1,264 @@ +%% Native CPS probes complement the F# tests: they exercise cancellation continuations, +%% callback exceptions, and deterministic cross-process handshakes without timing sleeps. +-module(async_lifecycle_tests). +-export([ + test_finalizers_settle_once/0, + test_finalizers_with_bind_propagate_continuation_exceptions/0, + test_bind_evaluation_failure_uses_error_continuation/0, + test_nested_cancellation_finalizers/0, + test_cancellation_finalizer_preserves_channel_and_exception/0, + test_using_settles_once/0, + test_using_binder_failure/0, + test_run_synchronously_pending_cancellation/0, + test_try_with_preserves_channels/0, + test_failure_racing_cancellation/0, + test_run_synchronously_timeout/0, + test_run_synchronously_timeout_preserves_supplied_token/0, + pending/1 +]). + +outcome(Comp) -> + try fable_async:run_synchronously(Comp) of + Value -> {ok, Value} + catch error:Reason -> {error, Reason} + end. + +body(success) -> fable_async_builder:return(42); +body(failure) -> fun(Ctx) -> (maps:get(on_error, Ctx))(body_defect) end; +body(throw) -> fun(_) -> erlang:error(body_defect) end; +body(cancel) -> fun(Ctx) -> (maps:get(on_cancel, Ctx))(ok) end. + +test_finalizers_settle_once() -> + lists:foreach(fun(Path) -> + lists:foreach(fun(Throws) -> + put(cleanups, 0), + Comp = fable_async_builder:try_finally(body(Path), fun(_) -> + put(cleanups, get(cleanups) + 1), + case Throws of true -> erlang:error(cleanup_defect); false -> ok end + end), + Result = outcome(Comp), + 1 = erase(cleanups), + case {Throws, Path, Result} of + {_, cancel, {error, #{exn_type := operation_canceled_exception}}} -> ok; + {true, _, {error, cleanup_defect}} when Path =/= cancel -> ok; + {false, success, {ok, 42}} -> ok; + {false, _, {error, body_defect}} when Path =/= cancel -> ok; + _ -> erlang:error({wrong_finalizer_result, Path, Throws, Result}) + end + end, [false, true]) + end, [success, failure, throw, cancel]), + %% A caller's throwing continuation cannot re-enter compensation. + put(cleanups, 0), + Comp = fable_async_builder:try_finally(body(success), fun(_) -> + put(cleanups, get(cleanups) + 1) + end), + {error, callback_defect} = + try Comp(#{ + on_success => fun(_) -> erlang:error(callback_defect) end, + on_error => fun(_) -> erlang:error(unexpected_error_callback) end, + on_cancel => fun(_) -> erlang:error(unexpected_cancel_callback) end, + cancel_token => undefined + }) + catch error:Reason -> {error, Reason} + end, + 1 = erase(cleanups), + ok. + +test_finalizers_with_bind_propagate_continuation_exceptions() -> + lists:foreach(fun(Path) -> + put(cleanups, 0), + put(callbacks, []), + Comp = fable_async_builder:try_finally( + fable_async_builder:bind(fable_async_builder:return(ok), fun(_) -> body(Path) end), + fun(_) -> put(cleanups, get(cleanups) + 1) end + ), + Callback = fun(Channel) -> fun(_) -> + put(callbacks, [Channel | get(callbacks)]), + erlang:error(callback_defect) + end end, + {error, callback_defect} = + try Comp(#{ + on_success => Callback(on_success), + on_error => Callback(on_error), + on_cancel => Callback(on_cancel), + cancel_token => undefined + }) + catch error:Reason -> {error, Reason} + end, + Expected = case Path of success -> on_success; failure -> on_error; cancel -> on_cancel end, + [Expected] = erase(callbacks), + 1 = erase(cleanups) + end, [success, failure, cancel]). + +test_bind_evaluation_failure_uses_error_continuation() -> + Comp = fable_async_builder:bind( + fable_async_builder:return(ok), fun(_) -> erlang:error(binder_defect) end + ), + Ref = make_ref(), + Comp(#{ + on_success => fun(_) -> erlang:error(unexpected_success_callback) end, + on_error => fun(Error) -> put(Ref, {error, Error}) end, + on_cancel => fun(_) -> erlang:error(unexpected_cancel_callback) end, + cancel_token => undefined + }), + {error, binder_defect} = erase(Ref), + ok. + +test_nested_cancellation_finalizers() -> + lists:foreach(fun(OuterThrows) -> + put(cleanups, []), + Inner = fable_async_builder:try_finally(body(cancel), fun(_) -> + put(cleanups, [inner | get(cleanups)]), + erlang:error(inner_defect) + end), + Outer = fable_async_builder:try_finally(Inner, fun(_) -> + put(cleanups, [outer | get(cleanups)]), + case OuterThrows of true -> erlang:error(outer_defect); false -> ok end + end), + {error, #{exn_type := operation_canceled_exception}} = outcome(Outer), + [outer, inner] = erase(cleanups) + end, [false, true]). + +test_cancellation_finalizer_preserves_channel_and_exception() -> + Cancellation = #{exn_type => operation_canceled_exception, message => <<"original cancellation">>}, + put(cleanups, 0), + Comp = fable_async_builder:try_finally( + fun(Ctx) -> (maps:get(on_cancel, Ctx))(Cancellation) end, + fun(_) -> put(cleanups, get(cleanups) + 1), erlang:error(cleanup_defect) end + ), + Ref = make_ref(), + Comp(#{ + on_success => fun(_) -> erlang:error(unexpected_success_callback) end, + on_error => fun(_) -> erlang:error(unexpected_error_callback) end, + on_cancel => fun(Error) -> put(Ref, {cancelled, Error}) end, + cancel_token => undefined + }), + {cancelled, Cancellation} = erase(Ref), + 1 = erase(cleanups), + ok. + +test_using_settles_once() -> + lists:foreach(fun(Path) -> + lists:foreach(fun(Arity) -> + lists:foreach(fun(Throws) -> + put(disposals, 0), + Dispose = fun() -> + put(disposals, get(disposals) + 1), + case Throws of true -> erlang:error(disposal_defect); false -> ok end + end, + Callback = case Arity of 0 -> Dispose; 1 -> fun(_) -> Dispose() end end, + Resource = #{dispose => Callback}, + Result = outcome(fable_async_builder:using(Resource, fun(_) -> body(Path) end)), + 1 = erase(disposals), + case {Throws, Path, Result} of + {_, cancel, {error, #{exn_type := operation_canceled_exception}}} -> ok; + {true, _, {error, disposal_defect}} when Path =/= cancel -> ok; + {false, success, {ok, 42}} -> ok; + {false, _, {error, body_defect}} when Path =/= cancel -> ok; + _ -> erlang:error({wrong_using_result, Path, Arity, Throws, Result}) + end + end, [false, true]) + end, [0, 1]) + end, [success, failure, throw, cancel]), + {ok, 42} = outcome(fable_async_builder:using(undefined, fun(_) -> body(success) end)), + ok. + +test_using_binder_failure() -> + put(disposals, 0), + Resource = #{dispose => fun(_) -> put(disposals, get(disposals) + 1) end}, + Comp = fable_async_builder:using(Resource, fun(_) -> erlang:error(binder_defect) end), + 0 = get(disposals), + {error, binder_defect} = outcome(Comp), + 1 = erase(disposals), + ok. + +%% Signal readiness only after registering the wake-up; cancellation can now interrupt +%% an admitted pending operation, regardless of scheduler interleaving. +pending(Parent) -> + fun(Ctx) -> + Owner = self(), + Ref = make_ref(), + Registration = fable_cancellation:register(maps:get(cancel_token, Ctx), fun(_) -> + Owner ! {cancel, Ref} + end), + Parent ! {pending, Owner}, + receive {cancel, Ref} -> ok after 5000 -> erlang:error(pending_not_cancelled) end, + fable_utils:safe_dispose(Registration), + (maps:get(on_cancel, Ctx))(ok) + end. + +test_run_synchronously_pending_cancellation() -> + lists:foreach(fun pending_cancellation/1, [undefined, -1, 5000]). + +pending_cancellation(Timeout) -> + Parent = self(), + Token = fable_cancellation:create(), + {Pid, Monitor} = spawn_monitor(fun() -> + Comp = fable_async_builder:bind(fable_async:cancellation_token(), fun(Actual) -> + Token = Actual, + pending(Parent) + end), + Result = try fable_async:run_synchronously(Comp, Timeout, Token) + catch error:Reason -> Reason end, + exit({result, Result}) + end), + try + receive + {pending, _Worker} -> ok; + {'DOWN', Monitor, process, Pid, EarlyReason} -> erlang:error({worker_failed_before_pending, EarlyReason}) + after 5000 -> erlang:error(worker_not_pending) + end, + fable_cancellation:cancel(Token), + receive + {'DOWN', Monitor, process, Pid, {result, #{exn_type := operation_canceled_exception}}} -> ok; + {'DOWN', Monitor, process, Pid, Reason} -> erlang:error({wrong_cancellation, Reason}) + after 5000 -> erlang:error(worker_not_completed) + end + after + exit(Pid, kill), + erlang:demonitor(Monitor, [flush]) + end. + +test_try_with_preserves_channels() -> + Comp = fable_async_builder:try_with(body(cancel), fun(_) -> + erlang:error(cancellation_entered_error_handler) + end), + {error, #{exn_type := operation_canceled_exception}} = outcome(Comp), + {error, #{exn_type := operation_canceled_exception}} = outcome(fable_async:catch_async(body(cancel))), + {ok, handled} = outcome(fable_async_builder:try_with(body(failure), fun(_) -> + fable_async_builder:return(handled) + end)), + ok. + +test_failure_racing_cancellation() -> + Token = fable_cancellation:create(), + Comp = fun(_) -> + fable_cancellation:cancel(Token), + erlang:error(real_defect) + end, + {error, real_defect} = try fable_async:run_synchronously(Comp, undefined, Token) + catch error:Reason -> {error, Reason} end, + false = fable_utils:is_exception_type(fable_async:wrap_error(<<"operation_cancelled">>), operation_canceled_exception), + ok. + +test_run_synchronously_timeout() -> + Parent = self(), + Comp = fable_async_builder:try_finally(pending(Parent), fun(_) -> Parent ! timeout_cleanup end), + {error, #{exn_type := timeout_exception}} = + try fable_async:run_synchronously(Comp, 1000, undefined) + catch error:Reason -> {error, Reason} end, + receive {pending, _} -> ok after 5000 -> erlang:error(timeout_worker_not_started) end, + receive timeout_cleanup -> ok after 5000 -> erlang:error(timeout_skipped_cleanup) end, + %% No late result or DOWN message may remain in the caller mailbox. + receive Message -> erlang:error({timeout_mailbox_leak, Message}) after 0 -> ok end. + +test_run_synchronously_timeout_preserves_supplied_token() -> + Token = fable_cancellation:create(), + Comp = fable_async_builder:bind(fable_async:cancellation_token(), fun(Actual) -> + Token = Actual, + fable_async_builder:return(42) + end), + 42 = fable_async:run_synchronously(Comp, 5000, Token), + {choice1_of2, 42} = fable_async:run_synchronously(fable_async:catch_async(Comp), 5000, Token), + false = fable_cancellation:is_cancellation_requested(Token), + ok. diff --git a/tests/Beam/erl/async_stacktrace_tests.erl b/tests/Beam/erl/async_stacktrace_tests.erl index 4b452701a7..46a52ea212 100644 --- a/tests/Beam/erl/async_stacktrace_tests.erl +++ b/tests/Beam/erl/async_stacktrace_tests.erl @@ -7,7 +7,9 @@ -export([ test_run_synchronously_keeps_original_stacktrace/0, test_handled_stacktrace_is_not_reused/0, - test_nested_run_keeps_own_stacktrace/0 + test_nested_run_keeps_own_stacktrace/0, + test_finalizer_failure_keeps_original_stacktrace/0, + test_start_immediate_downstream_failure_keeps_original_stacktrace/0 ]). %% The frame every test below looks for: an error raised at a known place in this module. @@ -73,3 +75,32 @@ test_nested_run_keeps_own_stacktrace() -> true -> ok; false -> erlang:error({expected_original_frame_on_top, Stacktrace}) end. + +test_finalizer_failure_keeps_original_stacktrace() -> + Comp = fable_async_builder:try_finally( + fable_async_builder:return(ok), fun(_) -> boom(cleanup_defect) end + ), + {error, cleanup_defect, Stacktrace} = run_catching(Comp), + case has_boom_on_top(Stacktrace) of + true -> ok; + false -> erlang:error({expected_finalizer_frame_on_top, Stacktrace}) + end. + +test_start_immediate_downstream_failure_keeps_original_stacktrace() -> + Comp = fable_async_builder:bind( + fable_async_builder:try_finally( + fable_async_builder:bind( + fable_async_builder:return(ok), fun(_) -> fable_async_builder:return(ok) end + ), + fun(_) -> ok end + ), + fun(_) -> fun(_) -> boom(downstream_defect) end end + ), + {error, downstream_defect, Stacktrace} = + try fable_async:start_immediate(Comp) + catch error:Err:St -> {error, Err, St} + end, + case has_boom_on_top(Stacktrace) of + true -> ok; + false -> erlang:error({expected_downstream_frame_on_top, Stacktrace}) + end. From 5d15141e519c2f320ac3457805b2d157cbea086f Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 12 Oct 2026 22:36:10 +0200 Subject: [PATCH 157/322] fix(all): support implicit conversion to erased unions (#5058) --- src/Fable.Transforms/Beam/Replacements.fs | 1 + src/Fable.Transforms/Replacements.Util.fs | 3 ++- src/fable-metadata/lib/Fable.Core.dll | Bin 256000 -> 268800 bytes tests/Beam/InteropTests.fs | 17 +++++++++++++++++ tests/Js/Main/UnionTypeTests.fs | 12 ++++++++++++ tests/Python/TestUnionType.fs | 13 +++++++++++++ tests/Rust/tests/src/UnionTests.fs | 7 +++++++ 7 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index ff1eedd1fc..f590ef4455 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5791,6 +5791,7 @@ let tryCall match info.DeclaringEntityFullName with // Implicit cast for erased unions (U2, U3...) via `!^`; the cast is erased. | _ when info.CompiledName = "op_ErasedCast" -> List.tryHead args + | Naming.StartsWith "Fable.Core.U" _ when info.CompiledName = "op_Implicit" -> List.tryHead args | "Microsoft.FSharp.Core.Operators" | "Microsoft.FSharp.Core.Operators.Checked" | "Microsoft.FSharp.Core.ExtraTopLevelOperators" diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 30009f5914..98df0aef12 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1298,7 +1298,8 @@ let (|RegexFlags|_|) e = [] let (|UniversalFableCoreHelpers|_|) (com: ICompiler) (ctx: Context) r t (i: CallInfo) args error = function - | "op_ErasedCast" -> + | "op_ErasedCast" + | "op_Implicit" -> match List.tryHead args with | Some x -> ValueSome x | None -> ValueNone diff --git a/src/fable-metadata/lib/Fable.Core.dll b/src/fable-metadata/lib/Fable.Core.dll index cabadd25bfb4f3260567e16a80efe0f8a9ae6345..ad480fca01b4b94d5671f136ba4a22eddb8e4a1d 100644 GIT binary patch literal 268800 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�==CmO~)h&M!*0$?pUU~vv$;-Q9C7nB=%mx~=L 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 EHwT}z;s5{u diff --git a/tests/Beam/InteropTests.fs b/tests/Beam/InteropTests.fs index 1c0d4282bf..749714d495 100644 --- a/tests/Beam/InteropTests.fs +++ b/tests/Beam/InteropTests.fs @@ -456,6 +456,23 @@ let ``test !^ implicit cast for erased unions works`` () = () #endif +[] +let ``test implicit conversion to erased unions works`` () = +#if FABLE_COMPILER + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" +#else + () +#endif + [] let ``test StringEnum works`` () = #if FABLE_COMPILER diff --git a/tests/Js/Main/UnionTypeTests.fs b/tests/Js/Main/UnionTypeTests.fs index 3c6a311449..002d57dcda 100644 --- a/tests/Js/Main/UnionTypeTests.fs +++ b/tests/Js/Main/UnionTypeTests.fs @@ -282,6 +282,18 @@ let tests = let x : U9> = !^42 x |> equal (U9.Case4 42) + + testCase "Implicit conversion to erased unions works" <| fun () -> + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" #endif testCase "Verify that union cases can have `-` in their names" <| fun () -> diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index a9775b681c..385e098bf2 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -205,6 +205,19 @@ let ``test Erased union type testing works`` () = U3.Case1 "HELLO" |> toString |> equal "HELLO" U3.Case2 3 |> toString |> equal "6" "HELLO" |> Wrapper |> U3.Case3 |> toString |> equal "OLLEH" + +[] +let ``test Implicit conversion to erased unions works`` () = + let strify (x: U3) = + match x with + | U3.Case1 i -> "i: " + string i + | U3.Case2 s -> "s: " + s + | U3.Case3 b -> if b then "b: yes" else "b: no" + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + strify 42 |> equal "i: 42" + strify "foo" |> equal "s: foo" + strify true |> equal "b: yes" #endif [] diff --git a/tests/Rust/tests/src/UnionTests.fs b/tests/Rust/tests/src/UnionTests.fs index f9cb755fde..7d1e4cd87a 100644 --- a/tests/Rust/tests/src/UnionTests.fs +++ b/tests/Rust/tests/src/UnionTests.fs @@ -476,4 +476,11 @@ let ``Compiler does not complain if the same type appears twice in nested U9`` ( U9> = !^42 x |> equal (U9.Case4 42) +[] +let ``Implicit conversion to erased unions works`` () = + let s: U2 = "foo" + s |> equal (U2.Case2 "foo") + let b: U3 = true + b |> equal (U3.Case3 true) + #endif From a8661874d60694f0faad235adb7f760fed6f72a5 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Mon, 12 Oct 2026 20:43:47 +0000 Subject: [PATCH 158/322] feat(php): System.Environment (#4782) Co-authored-by: Maxime Mangel --- .github/workflows/conventional-pr-title.yml | 2 +- src/fable-library-php/Environment.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/fable-library-php/Environment.php diff --git a/.github/workflows/conventional-pr-title.yml b/.github/workflows/conventional-pr-title.yml index ecd81d4aee..5b649294d9 100644 --- a/.github/workflows/conventional-pr-title.yml +++ b/.github/workflows/conventional-pr-title.yml @@ -21,4 +21,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: scopes: | - (?:all|(?:js|ts|dart|python|rust|beam|standalone)(?:/(?:js|ts|dart|python|rust|beam|standalone))*) + (?:all|(?:js|ts|dart|python|rust|beam|standalone|php)(?:/(?:js|ts|dart|python|rust|beam|standalone|php))*) diff --git a/src/fable-library-php/Environment.php b/src/fable-library-php/Environment.php new file mode 100644 index 0000000000..a43b598319 --- /dev/null +++ b/src/fable-library-php/Environment.php @@ -0,0 +1,12 @@ + Date: Mon, 12 Oct 2026 23:07:55 +0200 Subject: [PATCH 159/322] chore: release multiple projects (#5059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🤖 easybuild-shipit --- src/Fable.Cli/CHANGELOG.md | 15 ++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 15 ++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 11 ++++++++++- src/fable-compiler-js/package.json | 6 +++--- src/fable-metadata/CHANGELOG.md | 10 +++++++++- src/fable-metadata/package.json | 4 ++-- src/fable-standalone/CHANGELOG.md | 11 ++++++++++- src/fable-standalone/package.json | 4 ++-- 9 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index a1f83e8395..0f9ff4fbe8 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.21.0 - 2026-10-12 + +### 🚀 Features + +* *(php)* System.Environment (#4782) ([a8661874](https://github.com/fable-compiler/Fable/commit/a8661874d60694f0faad235adb7f760fed6f72a5)) + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 5.20.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 36dafa9ebf..9aa4b744e3 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.20.0 - 2026-10-12 + +### 🚀 Features + +* *(php)* System.Environment (#4782) ([a8661874](https://github.com/fable-compiler/Fable/commit/a8661874d60694f0faad235adb7f760fed6f72a5)) + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 5.19.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 1975606335..b54de2efec 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.20.1" + let VERSION = "5.21.0" [] let JS_LIBRARY_VERSION = "2.8.1" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 9f621a87b6..769095d23d 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.8.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 3.8.0 - 2026-10-12 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 3b43d76d9b..8d72702fc6 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.8.0", + "version": "3.8.1", "main": "index.js", "bin": { "fable": "index.js" @@ -24,8 +24,8 @@ }, "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { - "@fable-org/fable-metadata": "^2.1.0", - "@fable-org/fable-standalone": "^3.7.1" + "@fable-org/fable-metadata": "^2.1.1", + "@fable-org/fable-standalone": "^3.7.2" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-metadata/CHANGELOG.md b/src/fable-metadata/CHANGELOG.md index 204c735579..7de1806b18 100644 --- a/src/fable-metadata/CHANGELOG.md +++ b/src/fable-metadata/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: c977d78b39225a51c7bd051a1fe363ed0ccbe201 +last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 updaters: - package.json: file: package.json @@ -15,6 +15,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.1.1 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/c977d78b39225a51c7bd051a1fe363ed0ccbe201..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 2.1.0 - 2026-06-30 ### 🚀 Features diff --git a/src/fable-metadata/package.json b/src/fable-metadata/package.json index d7b04b7401..49ead91e6f 100644 --- a/src/fable-metadata/package.json +++ b/src/fable-metadata/package.json @@ -1,7 +1,7 @@ { "name": "@fable-org/fable-metadata", "private": false, - "version": "2.1.0", + "version": "2.1.1", "description": "Assemblies needed to compile F# projects with Fable", "main": "index.js", "type": "module", @@ -25,4 +25,4 @@ "dependencies": { "dirname-filename-esm": "^1.1.1" } -} +} \ No newline at end of file diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index a43a52ab4a..c3601aad4b 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 include: - ../Fable.Transforms/ - ../fcs-fable/ @@ -20,6 +20,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.7.2 - 2026-10-12 + +### 🐞 Bug Fixes + +* *(all)* Support implicit conversion to erased unions (#5058) ([5d15141e](https://github.com/fable-compiler/Fable/commit/5d15141e519c2f320ac3457805b2d157cbea086f)) +* *(beam)* Preserve async cancellation and finalize resources once (#5057) ([617f7111](https://github.com/fable-compiler/Fable/commit/617f7111bf02f8536b321b4373bdffa7a0f7d633)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..a8661874d60694f0faad235adb7f760fed6f72a5) + ## 3.7.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 8ef9f03e63..00ec7619fe 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.7.1", - "fableVersion": "5.20.1", + "version": "3.7.2", + "fableVersion": "5.21.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:37:58 -0700 Subject: [PATCH 160/322] fix(rust): Support inref struct unions (#5060) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 8 ++++-- tests/Rust/tests/src/UnionTests.fs | 36 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index f12cb78d84..a904a4efd2 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -3630,8 +3630,10 @@ module Util = mkLetExpr pat downcastExpr | _ -> makeLibCall com ctx genArgsOpt "Native" "type_test" [ expr ] - let makeUnionCasePatOpt (com: IRustCompiler) ctx typ nameOpt caseIndex = + let rec makeUnionCasePatOpt (com: IRustCompiler) ctx typ nameOpt caseIndex = match typ with + | Replacements.Util.IsInRefType com typ -> makeUnionCasePatOpt com ctx typ nameOpt caseIndex + | Fable.Option(genArg, _) -> // let genArgsOpt = transformGenArgs com ctx [genArg] let unionCaseFullName = [ "Some"; "None" ] |> List.item caseIndex |> rawIdent @@ -3652,6 +3654,7 @@ module Util = let pat = makeUnionCasePat unionCaseName fields Some(pat) + | Fable.DeclaredType(entRef, genArgs) -> let ent = com.GetEntity(entRef) @@ -3714,8 +3717,9 @@ module Util = mkIfThenElseExpr guardExpr thenExpr elseExpr let transformSwitch (com: IRustCompiler) ctx (evalExpr: Fable.Expr) cases defaultCase : Rust.Expr = - let namesForIndex evalType evalName caseIndex = //todo refactor with below + let rec namesForIndex evalType evalName caseIndex = //todo refactor with below match evalType with + | Replacements.Util.IsInRefType com typ -> namesForIndex typ evalName caseIndex | Fable.Option(genArg, _) -> match evalName with | Some idName -> diff --git a/tests/Rust/tests/src/UnionTests.fs b/tests/Rust/tests/src/UnionTests.fs index 7d1e4cd87a..1572ddf3f2 100644 --- a/tests/Rust/tests/src/UnionTests.fs +++ b/tests/Rust/tests/src/UnionTests.fs @@ -12,6 +12,26 @@ type Point2D = | Planar2D of float * float | Infinity +[] +type Shape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type Shape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + [] let ``Struct unions work`` () = let distance p = @@ -22,6 +42,22 @@ let ``Struct unions work`` () = let res = distance p res |> equal 5. +[] +let ``Struct unions with fields work`` () = + let radius = 3.0 + let circle = Shape.Circle radius + let square = Shape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``Struct unions without fields work`` () = + let radius = 3.0 + let circle = Shape2.Circle radius + let square = Shape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + [] let ``Union case matching works`` () = let x = IntCase1 5 From 9e78a3b327ca78d01863bf19b18c543673633af7 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 14 Oct 2026 01:03:32 +0200 Subject: [PATCH 161/322] fix(beam): make lazy sequence wrappers process-portable (#5063) --- src/Fable.Transforms/Beam/FABLE-BEAM.md | 9 +- .../Fable.Library.Beam.fsproj | 2 +- src/fable-library-beam/Seq.fs | 1410 +++++++++++++++++ tests/Beam/EnumerableTests.fs | 94 ++ 4 files changed, 1513 insertions(+), 2 deletions(-) create mode 100644 src/fable-library-beam/Seq.fs diff --git a/src/Fable.Transforms/Beam/FABLE-BEAM.md b/src/Fable.Transforms/Beam/FABLE-BEAM.md index 6b3e7896e8..2964d5833f 100644 --- a/src/Fable.Transforms/Beam/FABLE-BEAM.md +++ b/src/Fable.Transforms/Beam/FABLE-BEAM.md @@ -186,7 +186,7 @@ also sets `standard_io` and `standard_error` to Unicode. | `list<'T>` | list | Native linked list | | `array<'T>` | process-dictionary reference to a list | Mutable and process-local | | `byte[]` | `{byte_array, Size, AtomicsRef}` | O(1) reads and writes through `atomics` | -| `seq<'T>` | Fable sequence runtime | Compiled `Seq.fs`/`Seq2.fs` preserve lazy paths | +| `seq<'T>` | Fable sequence runtime | Lazy factory wrappers are immutable maps; cursor state is local to each enumeration | | `Map<'K,'V>` | map | Native key ordering applies | | `Set<'T>` | ordset | Sorted native list | | `option<'T>` | erased value or `undefined` | Ambiguous nested/generic cases use `{some, Value}` | @@ -343,6 +343,13 @@ process-dictionary-backed values: - mutable local keys are erased when their scope exits where code generation can prove the lifetime. +Lazy sequence factory wrappers are immutable maps and can cross processes when +their factories, sources, and captured values are process-portable. Enumeration +stays lazy: each factory call creates cursor state in the enumerating process. +Active enumerators with process-dictionary-backed state must remain in that +process. Captured mutable state, `Seq.cache`, and non-byte array or mutable +collection sources retain their process-local restrictions. + Module-level mutables use module-qualified process-dictionary keys and initialize through the generated module's `main/0`. Immutable module values that read a mutable are snapshotted in declaration order. This is correct only in a process that has run diff --git a/src/fable-library-beam/Fable.Library.Beam.fsproj b/src/fable-library-beam/Fable.Library.Beam.fsproj index beec4f5437..b87dc1a110 100644 --- a/src/fable-library-beam/Fable.Library.Beam.fsproj +++ b/src/fable-library-beam/Fable.Library.Beam.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs new file mode 100644 index 0000000000..bdb4b24110 --- /dev/null +++ b/src/fable-library-beam/Seq.fs @@ -0,0 +1,1410 @@ +// Adapted from: +// https://github.com/fsprojects/FSharpx.Extras/blob/master/src/FSharpx.Extras/ComputationExpressions/Enumerator.fs +// https://github.com/dotnet/fsharp/blob/main/src/fsharp/FSharp.Core/seq.fs +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +module SeqModule + +open Fable.Core + +type IEnumerator<'T> = System.Collections.Generic.IEnumerator<'T> +type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> + +module SR = + let enumerationAlreadyFinished = "Enumeration already finished." + let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputSequenceEmpty = "The input sequence was empty." + + let inputSequenceTooLong = "The input sequence contains more than one element." + + let keyNotFoundAlt = + "An index satisfying the predicate was not found in the collection." + + let notEnoughElements = "The input sequence has an insufficient number of elements." + + let resetNotSupported = "Reset is not supported on this enumerator." + +module Enumerator = + + let noReset () = + raise (System.NotSupportedException(SR.resetNotSupported)) + + let notStarted () = + raise (System.InvalidOperationException(SR.enumerationNotStarted)) + + let alreadyFinished () = + raise (System.InvalidOperationException(SR.enumerationAlreadyFinished)) + + [] + [] + type Enumerable<'T>(f) = + interface IEnumerable<'T> with + member x.GetEnumerator() = f () + + interface System.Collections.IEnumerable with + member x.GetEnumerator() = f () :> System.Collections.IEnumerator + + override _.ToString() = + let maxCount = 4 + let mutable i = 0 + let mutable str = "seq [" + // decision: calls the factory directly so the wrapper stays an immutable map portable across BEAM processes + use e: IEnumerator<'T> = f () + + while (i < maxCount && e.MoveNext()) do + if i > 0 then + str <- str + "; " + + str <- str + (string<'T> e.Current) + i <- i + 1 + + if i = maxCount then + str <- str + "; ..." + + str + "]" + + type FromFunctions<'T>(current, next, dispose) = + interface IEnumerator<'T> with + member _.Current = current () + + interface System.Collections.IEnumerator with + member _.Current = box (current ()) + member _.MoveNext() = next () + member _.Reset() = noReset () + + interface System.IDisposable with + member _.Dispose() = dispose () + + let inline fromFunctions current next dispose : IEnumerator<'T> = + new FromFunctions<_>(current, next, dispose) :> IEnumerator<'T> + + // // implementation for languages where arrays are not IEnumerable + // + // let empty<'T>(): IEnumerator<'T> = + // let mutable started = false + // let current() = if not started then notStarted() else alreadyFinished() + // let next() = started <- true; false + // let dispose() = () + // fromFunctions current next dispose + // + // let singleton (x: 'T): IEnumerator<'T> = + // let mutable index = -1 + // let current() = + // if index < 0 then notStarted() + // if index > 0 then alreadyFinished() + // x + // let next() = index <- index + 1; index = 0 + // let dispose() = () + // fromFunctions current next dispose + // + // let ofArray (arr: 'T[]): IEnumerator<'T> = + // let len = arr.Length + // let mutable i = -1 + // let current() = + // if i < 0 then notStarted() + // elif i >= len then alreadyFinished() + // else arr.[i] + // let next() = + // if i < len then + // i <- i + 1 + // i < len + // else false + // let dispose() = () + // fromFunctions current next dispose + + let cast (e: IEnumerator<'T>) : IEnumerator<'T> = + let current () = unbox<'T> e.Current + let next () = e.MoveNext() + let dispose () = e.Dispose() + + fromFunctions current next dispose + + let concat<'T, 'U when 'U :> seq<'T>> (sources: seq<'U>) = + let mutable outerOpt: IEnumerator<'U> option = None + let mutable innerOpt: IEnumerator<'T> option = None + let mutable started = false + let mutable finished = false + let mutable curr = None + + let current () = + if not started then + notStarted () + elif finished then + alreadyFinished () + + match curr with + | None -> alreadyFinished () + | Some x -> x + + let finish () = + finished <- true + + match innerOpt with + | None -> () + | Some inner -> + try + inner.Dispose() + finally + innerOpt <- None + + match outerOpt with + | None -> () + | Some outer -> + try + outer.Dispose() + finally + outerOpt <- None + + let loop () = + let mutable res = None + + while Option.isNone res do + match outerOpt, innerOpt with + | None, _ -> outerOpt <- Some(sources.GetEnumerator()) + | Some outer, None -> + if outer.MoveNext() then + let ie = outer.Current + innerOpt <- Some(ie.GetEnumerator()) + else + finish () + res <- Some false + | Some _, Some inner -> + if inner.MoveNext() then + curr <- Some(inner.Current) + res <- Some true + else + try + inner.Dispose() + finally + innerOpt <- None + + res.Value + + let next () = + if not started then + started <- true + + if finished then + false + else + loop () + + let dispose () = + if not finished then + finish () + + fromFunctions current next dispose + + let enumerateThenFinally f (e: IEnumerator<'T>) : IEnumerator<'T> = + let current () = e.Current + let next () = e.MoveNext() + + let dispose () = + try + e.Dispose() + finally + f () + + fromFunctions current next dispose + + let generateWhileSome (openf: unit -> 'T) (compute: 'T -> 'U option) (closef: 'T -> unit) : IEnumerator<'U> = + let mutable started = false + let mutable curr = None + let mutable state = Some(openf ()) + + let current () = + if not started then + notStarted () + + match curr with + | None -> alreadyFinished () + | Some x -> x + + let dispose () = + match state with + | None -> () + | Some x -> + try + closef x + finally + state <- None + + let finish () = + try + dispose () + finally + curr <- None + + let next () = + if not started then + started <- true + + match state with + | None -> false + | Some s -> + match + (try + compute s + with _ -> + finish () + reraise ()) + with + | None -> + finish () + false + | Some _ as x -> + curr <- x + true + + fromFunctions current next dispose + + let unfold (f: 'State -> ('T * 'State) option) (state: 'State) : IEnumerator<'T> = + let mutable curr: ('T * 'State) option = None + let mutable acc: 'State = state + + let current () = + match curr with + | None -> notStarted () + | Some(x, st) -> x + + let next () = + curr <- f acc + + match curr with + | None -> false + | Some(x, st) -> + acc <- st + true + + let dispose () = () + fromFunctions current next dispose + +// [] +// [] +// module Seq = + +let indexNotFound () = + raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) + +let mkSeq (f: unit -> IEnumerator<'T>) : seq<'T> = + Enumerator.Enumerable(f) :> IEnumerable<'T> + +let ofSeq (xs: seq<'T>) : IEnumerator<'T> = + let xs = nullArgCheck "source" xs + xs.GetEnumerator() + +let delay (generator: unit -> seq<'T>) = + mkSeq (fun () -> generator().GetEnumerator()) + +let concat<'Collection, 'T when 'Collection :> seq<'T>> (sources: seq<'Collection>) : seq<'T> = + mkSeq (fun () -> Enumerator.concat sources) + +let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = + mkSeq (fun () -> Enumerator.unfold generator state) + +let empty () = + delay (fun () -> Array.empty :> seq<'T>) + +let singleton x = + delay (fun () -> (Array.singleton x) :> seq<'T>) + +let ofArray (arr: 'T[]) = arr :> seq<'T> + +let toArray (xs: seq<'T>) : 'T[] = + match xs with + // | :? array<'T> as a -> Array.ofSeq a + | :? list<'T> as a -> Array.ofList a + | _ -> Array.ofSeq xs + +let ofList (xs: list<'T>) = (xs :> seq<'T>) + +let toList (xs: seq<'T>) : list<'T> = + match xs with + | :? array<'T> as a -> List.ofArray a + | :? list<'T> as a -> a + | _ -> List.ofSeq xs + +let generate create compute dispose = + mkSeq (fun () -> Enumerator.generateWhileSome create compute dispose) + +let generateIndexed create compute dispose = + mkSeq (fun () -> + let mutable i = -1 + + Enumerator.generateWhileSome + create + (fun x -> + i <- i + 1 + compute i x + ) + dispose + ) + +// let inline generateUsing (openf: unit -> ('U :> System.IDisposable)) compute = +// generate openf compute (fun (s: 'U) -> s.Dispose()) + +let append (xs: seq<'T>) (ys: seq<'T>) = concat [| xs; ys |] + +let cast (xs: IEnumerable<'T>) = + mkSeq (fun () -> + let xs = nullArgCheck "source" xs + xs.GetEnumerator() |> Enumerator.cast + ) + +let choose (chooser: 'T -> 'U option) (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + let mutable curr = None + + while (Option.isNone curr && e.MoveNext()) do + curr <- chooser e.Current + + curr + ) + (fun e -> e.Dispose()) + +let compareWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) (ys: seq<'T>) : int = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable c = 0 + let mutable b1 = e1.MoveNext() + let mutable b2 = e2.MoveNext() + + while c = 0 && b1 && b2 do + c <- comparer e1.Current e2.Current + + if c = 0 then + b1 <- e1.MoveNext() + b2 <- e2.MoveNext() + + if c <> 0 then + c + elif b1 then + 1 + elif b2 then + -1 + else + 0 + +let contains (value: 'T) (xs: seq<'T>) ([] comparer: System.Collections.Generic.IEqualityComparer<'T>) = + use e = ofSeq xs + let mutable found = false + + while (not found && e.MoveNext()) do + found <- comparer.Equals(value, e.Current) + + found + +let enumerateFromFunctions create moveNext current = + generate + create + (fun x -> + if moveNext x then + Some(current x) + else + None + ) + (fun x -> + match box (x) with + | :? System.IDisposable as id -> id.Dispose() + | _ -> () + ) + +let inline finallyEnumerable<'T> (compensation: unit -> unit, restf: unit -> seq<'T>) = + mkSeq (fun () -> + try + let e = restf () |> ofSeq + Enumerator.enumerateThenFinally compensation e + with _ -> + compensation () + reraise () + ) + +let enumerateThenFinally (source: seq<'T>) (compensation: unit -> unit) = + finallyEnumerable (compensation, (fun () -> source)) + +let enumerateUsing (resource: 'T :> System.IDisposable) (source: 'T -> #seq<'U>) : seq<'U> = + finallyEnumerable ( + (fun () -> + match box resource with + | null -> () + | _ -> resource.Dispose() + ), + (fun () -> source resource :> seq<_>) + ) + +let enumerateWhile (guard: unit -> bool) (xs: seq<'T>) = + concat ( + unfold + (fun i -> + if guard () then + Some(xs, i + 1) + else + None + ) + 0 + ) + +let enumerateTryWith (source: seq<'T>) (catchFilter: exn -> int) (catchHandler: exn -> seq<'T>) : seq<'T> = + mkSeq (fun () -> + let mutable e: IEnumerator<'T> option = None + let mutable caught = false // switched to the handler sequence + let mutable started = false + let mutable finished = false + let mutable curr = None + + let current () = + if not started then + Enumerator.notStarted () + elif finished then + Enumerator.alreadyFinished () + + match curr with + | None -> Enumerator.alreadyFinished () + | Some x -> x + + let rec tryNext () = + try + let en = + match e with + | Some en -> en + | None -> + let en = source.GetEnumerator() + e <- Some en + en + + if en.MoveNext() then + curr <- Some en.Current + true + else + finished <- true + false + with ex when not caught -> + if catchFilter ex <> 0 then + caught <- true + + match e with + | Some en -> + (try + en.Dispose() + with _ -> + ()) + + e <- None + | None -> () + + e <- Some((catchHandler ex).GetEnumerator()) + tryNext () + else + reraise () + + let next () = + if not started then + started <- true + + if finished then + false + else + tryNext () + + let dispose () = + match e with + | Some en -> en.Dispose() + | None -> () + + Enumerator.fromFunctions current next dispose + ) + +let filter f (xs: seq<'T>) = + xs + |> choose (fun x -> + if f x then + Some x + else + None + ) + +let exists predicate (xs: seq<'T>) = + use e = ofSeq xs + let mutable found = false + + while (not found && e.MoveNext()) do + found <- predicate e.Current + + found + +let exists2 (predicate: 'T1 -> 'T2 -> bool) (xs: seq<'T1>) (ys: seq<'T2>) = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable found = false + + while (not found && e1.MoveNext() && e2.MoveNext()) do + found <- predicate e1.Current e2.Current + + found + +let exactlyOne (xs: seq<'T>) = + use e = ofSeq xs + + if e.MoveNext() then + let v = e.Current + + if e.MoveNext() then + invalidArg "source" SR.inputSequenceTooLong + else + v + else + invalidArg "source" SR.inputSequenceEmpty + +let tryExactlyOne (xs: seq<'T>) = + use e = ofSeq xs + + if e.MoveNext() then + let v = e.Current + + if e.MoveNext() then + None + else + Some v + else + None + +let tryFind predicate (xs: seq<'T>) = + use e = ofSeq xs + let mutable res = None + + while (Option.isNone res && e.MoveNext()) do + let c = e.Current + + if predicate c then + res <- Some c + + res + +let find predicate (xs: seq<'T>) = + match tryFind predicate xs with + | Some x -> x + | None -> indexNotFound () + +let tryFindBack predicate (xs: seq<'T>) = + xs |> toArray |> Array.tryFindBack predicate + +let findBack predicate (xs: seq<'T>) = + match tryFindBack predicate xs with + | Some x -> x + | None -> indexNotFound () + +let tryFindIndex predicate (xs: seq<'T>) = + use e = ofSeq xs + + let rec loop i = + if e.MoveNext() then + if predicate e.Current then + Some i + else + loop (i + 1) + else + None + + loop 0 + +let findIndex predicate (xs: seq<'T>) = + match tryFindIndex predicate xs with + | Some x -> x + | None -> + indexNotFound () + -1 + +let tryFindIndexBack predicate (xs: seq<'T>) = + xs |> toArray |> Array.tryFindIndexBack predicate + +let findIndexBack predicate (xs: seq<'T>) = + match tryFindIndexBack predicate xs with + | Some x -> x + | None -> + indexNotFound () + -1 + +let fold<'T, 'State> (folder: 'State -> 'T -> 'State) (state: 'State) (xs: seq<'T>) = + use e = ofSeq xs + let mutable acc = state + + while e.MoveNext() do + acc <- folder acc e.Current + + acc + +let foldBack<'T, 'State> folder (xs: seq<'T>) state = + Array.foldBack folder (toArray xs) state + +let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'State) (xs: seq<'T1>) (ys: seq<'T2>) = + use e1 = ofSeq xs + use e2 = ofSeq ys + let mutable acc = state + + while e1.MoveNext() && e2.MoveNext() do + acc <- folder acc e1.Current e2.Current + + acc + +let foldBack2 (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: seq<'T1>) (ys: seq<'T2>) (state: 'State) = + let xs = toArray xs + let ys = toArray ys + let len = System.Math.Min(xs.Length, ys.Length) + let mutable acc = state + + for i = len - 1 downto 0 do + acc <- folder xs[i] ys[i] acc + + acc + +let forAll predicate xs = + not (exists (fun x -> not (predicate x)) xs) + +let forAll2 predicate xs ys = + not (exists2 (fun x y -> not (predicate x y)) xs ys) + +let tryHead (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.tryHead a + | :? list<'T> as a -> List.tryHead a + | _ -> + use e = ofSeq xs + + if e.MoveNext() then + Some(e.Current) + else + None + +let head (xs: seq<'T>) = + match tryHead xs with + | Some x -> x + | None -> invalidArg "source" SR.inputSequenceEmpty + +let initialize count f = + unfold + (fun i -> + if (i < count) then + Some(f i, i + 1) + else + None + ) + 0 + +let initializeInfinite f = initialize (System.Int32.MaxValue) f + +let isEmpty (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.isEmpty a + | :? list<'T> as a -> List.isEmpty a + | _ -> + use e = ofSeq xs + not (e.MoveNext()) + +let tryItem index (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.tryItem index a + | :? list<'T> as a -> List.tryItem index a + | _ -> + use e = ofSeq xs + + let rec loop index = + if not (e.MoveNext()) then + None + elif index = 0 then + Some e.Current + else + loop (index - 1) + + loop index + +let item index (xs: seq<'T>) = + match tryItem index xs with + | Some x -> x + | None -> invalidArg "index" SR.notEnoughElements + +let iterate action xs = fold (fun () x -> action x) () xs + +let iterate2 action xs ys = + fold2 (fun () x y -> action x y) () xs ys + +let iterateIndexed action xs = + fold + (fun i x -> + action i x + i + 1 + ) + 0 + xs + |> ignore + +let iterateIndexed2 action xs ys = + fold2 + (fun i x y -> + action i x y + i + 1 + ) + 0 + xs + ys + |> ignore + +let tryLast (xs: seq<'T>) = + // if isEmpty xs then None + // else Some (reduce (fun _ x -> x) xs) + use e = ofSeq xs + + let rec loop acc = + if not (e.MoveNext()) then + acc + else + loop e.Current + + if e.MoveNext() then + Some(loop e.Current) + else + None + +let last (xs: seq<'T>) = + match tryLast xs with + | Some x -> x + | None -> invalidArg "source" SR.notEnoughElements + +let length (xs: seq<'T>) = + match xs with + | :? array<'T> as a -> Array.length a + | :? list<'T> as a -> List.length a + | _ -> + use e = ofSeq xs + let mutable count = 0 + + while e.MoveNext() do + count <- count + 1 + + count + +let map (mapping: 'T -> 'U) (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + if e.MoveNext() then + Some(mapping e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let mapIndexed (mapping: int -> 'T -> 'U) (xs: seq<'T>) = + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if e.MoveNext() then + Some(mapping i e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let indexed (xs: seq<'T>) = xs |> mapIndexed (fun i x -> (i, x)) + +let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = + generate + (fun () -> (ofSeq xs, ofSeq ys)) + (fun (e1, e2) -> + if e1.MoveNext() && e2.MoveNext() then + Some(mapping e1.Current e2.Current) + else + None + ) + (fun (e1, e2) -> + try + e1.Dispose() + finally + e2.Dispose() + ) + +let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) = + generateIndexed + (fun () -> (ofSeq xs, ofSeq ys)) + (fun i (e1, e2) -> + if e1.MoveNext() && e2.MoveNext() then + Some(mapping i e1.Current e2.Current) + else + None + ) + (fun (e1, e2) -> + try + e1.Dispose() + finally + e2.Dispose() + ) + +let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = + generate + (fun () -> (ofSeq xs, ofSeq ys, ofSeq zs)) + (fun (e1, e2, e3) -> + if e1.MoveNext() && e2.MoveNext() && e3.MoveNext() then + Some(mapping e1.Current e2.Current e3.Current) + else + None + ) + (fun (e1, e2, e3) -> + try + e1.Dispose() + finally + try + e2.Dispose() + finally + e3.Dispose() + ) + +let readOnly (xs: seq<'T>) = + let xs = nullArgCheck "source" xs + map id xs + +type CachedSeq<'T>(cleanup, res: seq<'T>) = + interface System.IDisposable with + member _.Dispose() = cleanup () + + interface System.Collections.Generic.IEnumerable<'T> with + member _.GetEnumerator() = res.GetEnumerator() + + interface System.Collections.IEnumerable with + member _.GetEnumerator() = + (res :> System.Collections.IEnumerable).GetEnumerator() + + member _.Clear() = cleanup () + +// Adapted from https://github.com/dotnet/fsharp/blob/eb1337f218275da5294b5fbab2cf77f35ca5f717/src/fsharp/FSharp.Core/seq.fs#L971 +let cache (source: seq<'T>) = + let source = nullArgCheck "source" source + // Wrap a seq to ensure that it is enumerated just once and only as far as is necessary. + // + // This code is required to be thread safe. + // The necessary calls should be called at most once (include .MoveNext() = false). + // The enumerator should be disposed (and dropped) when no longer required. + //------ + // The state is (prefix,enumerator) with invariants: + // * the prefix followed by elts from the enumerator are the initial sequence. + // * the prefix contains only as many elements as the longest enumeration so far. + let prefix = ResizeArray<_>() + + // None = Unstarted. + // Some(Some e) = Started. + // Some None = Finished. + let mutable enumeratorR = None + + let oneStepTo i = + // If possible, step the enumeration to prefix length i (at most one step). + // Be speculative, since this could have already happened via another thread. + if i >= prefix.Count then // is a step still required? + // If not yet started, start it (create enumerator). + let optEnumerator = + match enumeratorR with + | None -> + let optEnumerator = Some(source.GetEnumerator()) + enumeratorR <- Some optEnumerator + optEnumerator + | Some optEnumerator -> optEnumerator + + match optEnumerator with + | Some enumerator -> + if enumerator.MoveNext() then + prefix.Add(enumerator.Current) + else + enumerator.Dispose() // Move failed, dispose enumerator, + enumeratorR <- Some None // drop it and record finished. + | None -> () + + let result = + unfold + (fun i -> + // i being the next position to be returned + // A lock is needed over the reads to prefix.Count since the list may be being resized + // NOTE: we could change to a reader/writer lock here + lock prefix + <| fun () -> + if i < prefix.Count then + Some(prefix.[i], i + 1) + else + oneStepTo i + + if i < prefix.Count then + Some(prefix.[i], i + 1) + else + None + ) + 0 + + let cleanup () = + lock prefix + <| fun () -> + prefix.Clear() + + match enumeratorR with + | Some(Some e) -> e.Dispose() + | _ -> () + + enumeratorR <- None + + (new CachedSeq<_>(cleanup, result) :> seq<_>) + +let allPairs (xs: seq<'T1>) (ys: seq<'T2>) : seq<'T1 * 'T2> = + let ysCache = cache ys + + delay (fun () -> + let mapping (x: 'T1) = ysCache |> map (fun y -> (x, y)) + concat (map mapping xs) + ) + +let mapFold (mapping: 'State -> 'T -> 'Result * 'State) state (xs: seq<'T>) = + let arr, state = Array.mapFold mapping state (toArray xs) + readOnly arr, state + +let mapFoldBack (mapping: 'T -> 'State -> 'Result * 'State) (xs: seq<'T>) state = + let arr, state = Array.mapFoldBack mapping (toArray xs) state + readOnly arr, state + +let tryPick chooser (xs: seq<'T>) = + use e = ofSeq xs + let mutable res = None + + while (Option.isNone res && e.MoveNext()) do + res <- chooser e.Current + + res + +let pick chooser (xs: seq<'T>) = + match tryPick chooser xs with + | Some x -> x + | None -> indexNotFound () + +let reduce folder (xs: seq<'T>) = + use e = ofSeq xs + + let rec loop acc = + if e.MoveNext() then + loop (folder acc e.Current) + else + acc + + if e.MoveNext() then + loop e.Current + else + invalidOp SR.inputSequenceEmpty + +let reduceBack folder (xs: seq<'T>) = + let arr = toArray xs + + if arr.Length > 0 then + Array.reduceBack folder arr + else + invalidOp SR.inputSequenceEmpty + +let replicate n x = initialize n (fun _ -> x) + +let reverse (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.rev |> ofArray) + +let scan folder (state: 'State) (xs: seq<'T>) = + delay (fun () -> + let first = singleton state + let mutable acc = state + + let rest = + xs + |> map (fun x -> + acc <- folder acc x + acc + ) + + [| first; rest |] |> concat + ) + +let scanBack folder (xs: seq<'T>) (state: 'State) = + delay (fun () -> + let arr = toArray xs + Array.scanBack folder arr state |> ofArray + ) + +let skip count (source: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq source + + try + for _ = 1 to count do + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) + +let skipWhile predicate (xs: seq<'T>) = + delay (fun () -> + let mutable skipped = true + + xs + |> filter (fun x -> + if skipped then + skipped <- predicate x + + not skipped + ) + ) + +let tail (xs: seq<'T>) = skip 1 xs + +let take count (xs: seq<'T>) = + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if i < count then + if e.MoveNext() then + Some(e.Current) + else + invalidArg "source" SR.notEnoughElements + else + None + ) + (fun e -> e.Dispose()) + +let takeWhile predicate (xs: seq<'T>) = + generate + (fun () -> ofSeq xs) + (fun e -> + if e.MoveNext() && predicate e.Current then + Some(e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let truncate count (xs: seq<'T>) = + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if i < count && e.MoveNext() then + Some(e.Current) + else + None + ) + (fun e -> e.Dispose()) + +let zip (xs: seq<'T1>) (ys: seq<'T2>) = map2 (fun x y -> (x, y)) xs ys + +let zip3 (xs: seq<'T1>) (ys: seq<'T2>) (zs: seq<'T3>) = map3 (fun x y z -> (x, y, z)) xs ys zs + +let collect<'T, 'Collection, 'U when 'Collection :> 'U seq> (mapping: 'T -> 'Collection) (xs: seq<'T>) : seq<'U> = + delay (fun () -> xs |> map mapping |> concat) + +let where predicate (xs: seq<'T>) = filter predicate xs + +let pairwise (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) + +let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) + +let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) + +let transpose (xss: seq<#seq<'T>>) : seq> = + delay (fun () -> + xss + |> toArray + |> Array.map toArray + |> Array.transpose + |> Array.map ofArray + |> ofArray + ) + +let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = + delay (fun () -> + let arr = toArray xs + Array.sortInPlaceWith comparer arr // Note: In JS this sort is stable + arr |> ofArray + ) + +let sort (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) = + sortWith (fun (x: 'T) (y: 'T) -> comparer.Compare(x, y)) xs + +let sortBy (projection: 'T -> 'U) (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'U>) = + sortWith (fun x y -> comparer.Compare(projection x, projection y)) xs + +let sortDescending (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) = + sortWith (fun (x: 'T) (y: 'T) -> comparer.Compare(x, y) * -1) xs + +let sortByDescending + (projection: 'T -> 'U) + (xs: seq<'T>) + ([] comparer: System.Collections.Generic.IComparer<'U>) + = + sortWith (fun x y -> comparer.Compare(projection x, projection y) * -1) xs + +let sum (xs: seq<'T>) ([] adder: IGenericAdder<'T>) : 'T = + fold (fun acc x -> adder.Add(acc, x)) (adder.GetZero()) xs + +let sumBy (f: 'T -> 'U) (xs: seq<'T>) ([] adder: IGenericAdder<'U>) : 'U = + fold (fun acc x -> adder.Add(acc, f x)) (adder.GetZero()) xs + +let maxBy (projection: 'T -> 'U) xs ([] comparer: System.Collections.Generic.IComparer<'U>) : 'T = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + y + else + x + ) + xs + +let max xs ([] comparer: System.Collections.Generic.IComparer<'T>) : 'T = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + y + else + x + ) + xs + +let minBy (projection: 'T -> 'U) xs ([] comparer: System.Collections.Generic.IComparer<'U>) : 'T = + reduce + (fun x y -> + if comparer.Compare(projection y, projection x) > 0 then + x + else + y + ) + xs + +let min (xs: seq<'T>) ([] comparer: System.Collections.Generic.IComparer<'T>) : 'T = + reduce + (fun x y -> + if comparer.Compare(y, x) > 0 then + x + else + y + ) + xs + +let average (xs: seq<'T>) ([] averager: IGenericAverager<'T>) : 'T = + let mutable count = 0 + + let folder acc x = + count <- count + 1 + averager.Add(acc, x) + + let total = fold folder (averager.GetZero()) xs + + if count = 0 then + invalidArg "source" SR.inputSequenceEmpty + else + averager.DivideByInt(total, count) + +let averageBy (f: 'T -> 'U) (xs: seq<'T>) ([] averager: IGenericAverager<'U>) : 'U = + let mutable count = 0 + + let inline folder acc x = + count <- count + 1 + averager.Add(acc, f x) + + let total = fold folder (averager.GetZero()) xs + + if count = 0 then + invalidArg "source" SR.inputSequenceEmpty + else + averager.DivideByInt(total, count) + +let permute f (xs: seq<'T>) = + delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) + +let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) + +// let init = initialize +// let initInfinite = initializeInfinite +// let iter = iterate +// let iter2 = iterate2 +// let iteri = iterateIndexed +// let iteri2 = iterateIndexed2 +// let forall = forAll +// let forall2 = forAll2 +// let mapi = mapIndexed +// let mapi2 = mapIndexed2 +// let readonly = readOnly +// let rev = reverse + +let insertAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index then + isDone <- true + Some y + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let insertManyAt (index: int) (ys: seq<'T>) (xs: seq<'T>) : seq<'T> = + // incomplete -1, in-progress 0, complete 1 + let mutable status = -1 + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs, ofSeq ys) + (fun i (e1, e2) -> + if i = index then + status <- 0 + + let inserted = + if status = 0 then + if e2.MoveNext() then + Some e2.Current + else + status <- 1 + None + else + None + + match inserted with + | Some inserted -> Some inserted + | None -> + if e1.MoveNext() then + Some e1.Current + else + if status < 1 then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun (e1, e2) -> + e1.Dispose() + e2.Dispose() + ) + +let removeAt (index: int) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index && e.MoveNext() then + isDone <- true + + if e.MoveNext() then + Some e.Current + else + None + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if i < index then + if e.MoveNext() then + Some e.Current + else + invalidArg "index" SR.indexOutOfBounds + else + if i = index then + for _ = 1 to count do + if not (e.MoveNext()) then + invalidArg "count" SR.indexOutOfBounds + + if e.MoveNext() then + Some e.Current + else + None + ) + (fun e -> e.Dispose()) + +let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = + let mutable isDone = false + + if index < 0 then + invalidArg "index" SR.indexOutOfBounds + + generateIndexed + (fun () -> ofSeq xs) + (fun i e -> + if (isDone || i < index) && e.MoveNext() then + Some e.Current + elif i = index && e.MoveNext() then + isDone <- true + Some y + else + if not isDone then + invalidArg "index" SR.indexOutOfBounds + + None + ) + (fun e -> e.Dispose()) + +let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = + let arr = toArray xs + Array.randomShuffleInPlaceBy randomizer arr + ofArray arr + +let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = + randomShuffleBy (fun () -> random.NextDouble()) xs + +let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs + +let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = + xs |> toArray |> Array.randomChoiceBy randomizer + +let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = + xs |> toArray |> Array.randomChoiceWith random + +let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice + +let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = + xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + +let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = + randomChoicesBy (fun () -> random.NextDouble()) count xs + +let randomChoices (count: int) (xs: 'T seq) : 'T seq = + randomChoicesWith (System.Random()) count xs + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = + xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray + +let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = + randomSampleBy (fun () -> random.NextDouble()) count xs + +let randomSample (count: int) (xs: 'T seq) : 'T seq = + randomSampleWith (System.Random()) count xs diff --git a/tests/Beam/EnumerableTests.fs b/tests/Beam/EnumerableTests.fs index ad27923e0f..259565e84f 100644 --- a/tests/Beam/EnumerableTests.fs +++ b/tests/Beam/EnumerableTests.fs @@ -67,3 +67,97 @@ let ``test Enumerator can be converted to seq`` () = |> Seq.toList |> List.sum |> equal 55 + +#if FABLE_COMPILER_BEAM +// decision: enumerates in another process to detect process-local sequence wrapper state +[ + Parent = self(), F = $0, + {Pid, Ref} = spawn_monitor(fun() -> + Result = try + {ok, case erlang:fun_info(F, arity) of {arity, 0} -> F(); _ -> F(ok) end} + catch Class:Reason:Stack -> {error, Class, Reason, Stack} end, + Parent ! {self(), Result} + end), + receive + {Pid, Result} -> + receive {'DOWN', Ref, process, Pid, normal} -> ok + after 5000 -> erlang:error(sequence_process_timeout) end, + case Result of + {ok, Value} -> Value; + {error, C, E, S} -> erlang:raise(C, E, S) + end; + {'DOWN', Ref, process, Pid, Reason} -> erlang:error({sequence_process_failed, Reason}) + after 5000 -> erlang:error(sequence_process_timeout) + end +end)() +""")>] +let private inSequenceProcess (f: unit -> 'T) : 'T = Fable.Core.Util.nativeOnly + +// decision: calls the runtime override directly because seq-typed ToString uses the general converter +[] +let private sequenceToString (values: seq<'T>) : string = Fable.Core.Util.nativeOnly + +[] +let ``test lazy sequence wrappers are portable across processes`` () = + let values = seq { 1..3 } + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + inSequenceProcess (fun () -> sequenceToString values) |> equal "seq [1; 2; 3]" + +[] +let ``test mapped sequence wrappers are portable across processes`` () = + let values = seq { 1..3 } |> Seq.map (fun x -> x * 2) + inSequenceProcess (fun () -> Seq.toList values) |> equal [2; 4; 6] + +[] +let ``test lazy sequence survives its creating process`` () = + let values = inSequenceProcess (fun () -> seq { 1..3 }) + Seq.toList values |> equal [1; 2; 3] + inSequenceProcess (fun () -> Seq.toList values) |> equal [1; 2; 3] + +[] +let ``test sequence ToString preserves formatting`` () = + Seq.empty |> sequenceToString |> equal "seq []" + Seq.singleton 1 |> sequenceToString |> equal "seq [1]" + seq { 1..3 } |> sequenceToString |> equal "seq [1; 2; 3]" + seq { 1..4 } |> sequenceToString |> equal "seq [1; 2; 3; 4; ...]" + Seq.initInfinite (fun i -> i + 1) |> sequenceToString |> equal "seq [1; 2; 3; 4; ...]" + +[] +let ``test sequence ToString is lazy repeatable and disposes its enumerator`` () = + let mutable evaluated = 0 + let mutable disposed = 0 + let values = seq { + try + for value in 1..5 do + evaluated <- evaluated + 1 + yield value + finally + disposed <- disposed + 1 + } + evaluated |> equal 0 + disposed |> equal 0 + sequenceToString values |> equal "seq [1; 2; 3; 4; ...]" + evaluated |> equal 4 + disposed |> equal 1 + Seq.toList values |> equal [1; 2; 3; 4; 5] + evaluated |> equal 9 + disposed |> equal 2 + sequenceToString values |> equal "seq [1; 2; 3; 4; ...]" + evaluated |> equal 13 + disposed |> equal 3 + +[] +let ``test sequence ToString disposes its enumerator when enumeration throws`` () = + let mutable disposed = 0 + let values = seq { + try + yield 1 + failwith "sequence failure" + finally + disposed <- disposed + 1 + } + Util.throwsError "sequence failure" (fun () -> sequenceToString values) + disposed |> equal 1 +#endif From 5218693738defc8a4502d28c206e391b241f9fd8 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 14 Oct 2026 20:14:10 +0200 Subject: [PATCH 162/322] fix(python): settle cancelled tasks awaited by async workflows (#5064) --- src/Fable.Transforms/Python/Replacements.fs | 8 +++ src/fable-library-py/fable_library/async_.py | 13 +++++ .../fable_library/async_builder.py | 6 +++ tests/Python/TestAsync.fs | 42 ++++++++------- tests/Python/test_async_runtime.py | 51 ++++++++++++++++++- 5 files changed, 101 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 1513e1a161..5ef2df57fe 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -782,6 +782,8 @@ let tryEntityIdent (com: Compiler) entFullName = // | BuiltinDefinition FSharpMap _ -> fail "Map" // TODO: | Types.matchFail -> makeImportLib com Any "MatchFailureException" "Types" |> Some | Types.exception_ -> makeIdentExpr "Exception" |> Some + | "System.Threading.Tasks.TaskCanceledException" -> + makeImportLib com Any "TaskCanceledError" "async_builder" |> Some // | "System.Collections.Generic.KeyNotFoundException" -> // makeImportLib com Any "KeyNotFoundException" "System.Collections.Generic" |> Some | BuiltinSystemException entName -> makeImportLib com Any entName "System" |> Some @@ -2844,6 +2846,9 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr match i.CompiledName, thisArg with | ".ctor", _ -> match i.DeclaringEntityFullName with + | "System.Threading.Tasks.TaskCanceledException" -> + Helper.LibCall(com, "async_builder", "TaskCanceledError", t, args, isConstructor = true, ?loc = r) + |> Some // | "System.Collections.Generic.KeyNotFoundException" | BuiltinSystemException _ -> bclType com ctx r t i thisArg args | _ -> @@ -3675,6 +3680,9 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with + | "AwaitTask" -> + Helper.LibCall(com, "async_", "await_task_with_cancellation", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | "FromContinuations" when (match t with | Fable.DeclaredType(_, [ Fable.Unit ]) -> true diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index ff1a7a03f6..0e06c6fffd 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -16,6 +16,7 @@ Continuations, IAsyncContext, OperationCanceledError, + TaskCanceledError, Trampoline, empty_continuation, protected_bind, @@ -281,6 +282,17 @@ def callback(conts: Continuations[T]) -> None: return from_continuations(callback) +def await_task_with_cancellation[T](task: Awaitable[T]) -> Async[T]: + async def observe() -> T: + try: + return await task + except asyncio.CancelledError as error: + # decision: cancelled tasks are ordinary AwaitTask failures, matching .NET TaskCanceledException + raise TaskCanceledError() from error + + return await_task(observe()) + + def run_in_loop(computation: Callable[..., None]) -> Any: """Run a computation on the event loop. @@ -461,6 +473,7 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) __all__ = [ "await_event", "await_task", + "await_task_with_cancellation", "cancel", "cancel_after", "cancellation_token", diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index 377aa71481..c3eef16f2d 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -20,6 +20,12 @@ def __init__(self, msg: str | None = None) -> None: super().__init__(msg or "The operation was canceled") +class TaskCanceledError(OperationCanceledError): + def __init__(self, msg: str | None = None, inner_exception: Exception | None = None) -> None: + Exception.__init__(self, "A task was canceled." if msg is None else msg) + self.inner_exception: Exception | None = inner_exception + + type Continuations[T] = tuple[ Callable[[T], None], Callable[[Exception], None], diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index 3c8563ae9e..713af3d5c6 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -345,26 +345,34 @@ let ``test Interaction between Async and Task works`` () = equal true res } |> Async.RunSynchronously -#if FABLE_COMPILER [] let ``test Tasks can be cancelled`` () = async { - let mutable res = 0 - let tcs = new System.Threading.CancellationTokenSource(50) - let work = - let work = async { - do! Async.Sleep 75 - res <- -1 - } - Async.StartAsTask(work, cancellationToken=tcs.Token) |> Async.AwaitTask - // behavior change: a cancelled task is now triggering the exception continuation instead of - // the cancellation continuation, see: https://github.com/Microsoft/visualfsharp/issues/1416 - // also, System.OperationCanceledException will be changed to TaskCanceledException (not yet) - Async.StartWithContinuations(work, ignore, ignore, (fun _ -> res <- 1)) - do! Async.Sleep 100 - equal 1 res - } |> Async.StartImmediate -#endif + use source = new System.Threading.CancellationTokenSource() + let entered = System.Threading.Tasks.TaskCompletionSource() + let work = async { + entered.SetResult 1 + do! Async.Sleep Int32.MaxValue + failwith "Cancelled task resumed" + } + let task = Async.StartAsTask(work, cancellationToken=source.Token) + let! _ = Async.AwaitTask entered.Task + source.Cancel() + let! result = Async.Catch(Async.AwaitTask task) + match result with + | Choice2Of2 (:? System.Threading.Tasks.TaskCanceledException) -> () + | _ -> failwith "Expected TaskCanceledException through the error continuation" + } |> Async.RunSynchronously + +[] +let ``test TaskCanceledException preserves message and inner exception`` () = + let inner = Exception("inner") + let error: exn = System.Threading.Tasks.TaskCanceledException("task failure", inner) + match error with + | :? System.Threading.Tasks.TaskCanceledException -> + equal "task failure" error.Message + equal true (obj.ReferenceEquals(inner, error.InnerException)) + | _ -> failwith "Expected TaskCanceledException" [] let ``test Deep recursion with async doesn't cause stack overflow`` () = diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py index 3016c43571..4e04b809a1 100644 --- a/tests/Python/test_async_runtime.py +++ b/tests/Python/test_async_runtime.py @@ -2,8 +2,15 @@ from threading import Barrier, Condition, Event, Thread import pytest -from fable_library.async_ import await_event, sleep, start_with_continuations -from fable_library.async_builder import CancellationCallbackError, CancellationToken, IAsyncContext, Trampoline +from fable_library.async_ import await_event, await_task_with_cancellation, sleep, start_with_continuations +from fable_library.async_builder import ( + CancellationCallbackError, + CancellationToken, + IAsyncContext, + OperationCanceledError, + TaskCanceledError, + Trampoline, +) from fable_library.event import Event as FableEvent @@ -165,6 +172,46 @@ def test_sleep_on_cancelled_token_settles_once(): assert calls == ["cancel"] +@pytest.mark.parametrize("already_cancelled", [False, True]) +def test_await_task_cancellation_uses_error_continuation(already_cancelled: bool) -> None: + async def run() -> None: + loop = asyncio.get_running_loop() + unhandled: list[object] = [] + loop.set_exception_handler(lambda _, context: unhandled.append(context.get("exception"))) + task: asyncio.Future[int] = loop.create_future() + token = CancellationToken() + calls: list[str] = [] + errors: list[Exception] = [] + if already_cancelled: + task.cancel() + + def on_error(error: Exception) -> None: + calls.append("error") + errors.append(error) + + start_with_continuations( + await_task_with_cancellation(task), + lambda _: calls.append("success"), + on_error, + lambda _: calls.append("cancel"), + token, + ) + await asyncio.sleep(0) + if not already_cancelled: + task.cancel() + await asyncio.sleep(0) + await asyncio.sleep(0) + assert calls == ["error"] + assert len(errors) == 1 + assert isinstance(errors[0], TaskCanceledError) + assert isinstance(errors[0], OperationCanceledError) + assert errors[0].inner_exception is None + assert not token.is_cancelled + assert not unhandled + + asyncio.run(run()) + + class _DeferringTrampoline(Trampoline): def increment_and_check(self) -> bool: return True From 288c69f8b8e6ae76037ea61ebeac42f6a3ea169d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:29:27 +0200 Subject: [PATCH 163/322] chore: Bump the all-pip group in /src/fable-library-py with 2 updates (#5067) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/uv.lock | 429 +++++++++++++++++++---------------- 1 file changed, 230 insertions(+), 199 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index ec6c98dbc3..aa0fe555fa 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -61,98 +61,98 @@ dev = [ [[package]] name = "hypothesis" -version = "6.168.3" +version = "6.168.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/a8/bd70d7c2966e561228b9fdc075ee77c0ba577dcbbfbf921edf614db14f6a/hypothesis-6.168.5.tar.gz", hash = "sha256:76b9226962fe11d40858253a967eda95bb65811365286317e0118f4ec8f808c7", size = 511115, upload-time = "2026-10-05T23:26:35.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, - { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, - { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, - { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, - { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, - { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, - { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, - { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, - { url = "https://files.pythonhosted.org/packages/bd/80/4efe245037b2d2ff64e220fecb4656f41a3d5e296a65daa6d6e4245e1400/hypothesis-6.168.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2", size = 792323, upload-time = "2026-09-28T05:19:31.294Z" }, - { url = "https://files.pythonhosted.org/packages/49/cd/0f96010943e93d07fe0c064a6c1812b33d305f29fa43606a8b863ba806af/hypothesis-6.168.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742", size = 788335, upload-time = "2026-09-28T05:20:29.748Z" }, - { url = "https://files.pythonhosted.org/packages/12/a9/95ebd471169fef0ccb7c9194ed643797eab67703b927efb6e76c046a78ce/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05", size = 1124433, upload-time = "2026-09-28T05:19:01.479Z" }, - { url = "https://files.pythonhosted.org/packages/9c/3a/153bf04b7ce7f14d563b943af5f05347d5f91948f66f59655b9913b97531/hypothesis-6.168.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74", size = 1170640, upload-time = "2026-09-28T05:18:40.699Z" }, - { url = "https://files.pythonhosted.org/packages/d9/da/3d5ca014d00897dc5f69fa45374d86014fde89938e381303d597275b4ad3/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c", size = 1300447, upload-time = "2026-09-28T05:18:33.251Z" }, - { url = "https://files.pythonhosted.org/packages/18/76/38fb90efa47cab7d93b2794624a8177533bc748ee4666681a0ece1e3c23b/hypothesis-6.168.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083", size = 1336918, upload-time = "2026-09-28T05:18:45.505Z" }, - { url = "https://files.pythonhosted.org/packages/78/44/92b10c9df3029379cc67ad2d3af17cd1b35a72516b0ef37d5b55a125caec/hypothesis-6.168.3-cp310-cp310-win_amd64.whl", hash = "sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0", size = 684844, upload-time = "2026-09-28T05:19:39.335Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, - { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, - { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, - { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, - { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, - { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, - { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, - { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, - { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, - { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, - { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, - { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, - { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, - { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, - { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, - { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, - { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, - { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, - { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, - { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, - { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, - { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, - { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, - { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, - { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, - { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/7f04c8d277dfc828ba584b7d9d10dbac5e91fce673fa5328f7bd5bf64609/hypothesis-6.168.5-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ca43a751410a9c6685f029fd5126cc5507664cafaa76017922aa8ae2e17b6620", size = 791539, upload-time = "2026-10-05T23:24:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/11/5c/660906d83db74eb86feda715d0f2df14836205b14a183332116676733e6f/hypothesis-6.168.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b98707cbe9f430d100a945bbe17612fd3aa44eac1b0ac5299669fe3b8e4128", size = 787208, upload-time = "2026-10-05T23:25:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/01/85/36e19492bc4ff354c2be9c8fa7c6ace0c65f9d2c7116656b741680c6ca55/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dde52a0b696c642e7f988a03026c7c29f90daf21e74507b6f865c3ccc9d536e", size = 1115200, upload-time = "2026-10-05T23:25:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/d4/82/3273fb0a3567c09b767bb8fe2824d65e16ae2abb92cf1f43762df723df94/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42f02e4541fe0c17a1320617effc0ab8a8aca2a9af15e3358d4150acf3bbdc00", size = 1145113, upload-time = "2026-10-05T23:25:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/74/59/5c5904555a0bbd4b2898d73ea90c6d03f5be0d8ff0756ac1d519ace6ae66/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf6dd7e537a12763c9afa017f7a6159e5cda608e98670621fa44596a1e8e9288", size = 1139564, upload-time = "2026-10-05T23:25:56.681Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/55654ff9575587a401e304f08ad1d43b7e6318f81c66bd866fdc5ab4665b/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df2c04cd30abf42c52580184216162a75b5508b214a472b86670f6dd50659a3b", size = 1192142, upload-time = "2026-10-05T23:26:06.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/91/4cc9d6e8a950473e07e3ebf00cbb8ee0d76b14d193f94c3de20f1c09e2b1/hypothesis-6.168.5-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278662eb21aaec9eaae71ea4dabd4fe390c2af11ec58a6a0606687cf6d7689b0", size = 1157262, upload-time = "2026-10-05T23:24:59.229Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/4b8aa3be788ea81b9a7bc6b673ed89edd72fd0645c6aa691d4c159ff971a/hypothesis-6.168.5-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:6bcedc4ab8ab92dd0f3af0cfe24dce184d225751d7bc870a9cddb9a557de847f", size = 1113779, upload-time = "2026-10-05T23:24:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/f9/98/2eb4c79d1851195e6a083568b065235680ab984e984bbd472f2a7d02ba33/hypothesis-6.168.5-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b58097cc3b98d8616f635ac73888fc9f859311875f2adc043f1544c40c3c466", size = 1152639, upload-time = "2026-10-05T23:25:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9c/68f7e99b43c6f37c077669a4d3bd88f48c042444ced9e7cff0eaf44bc70a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f8a387d9ee7f804e830b31f2e2e339ab5731665e922cfda4f6f6fbdb05e191b4", size = 1290068, upload-time = "2026-10-05T23:25:28.45Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/d4f87164a0d028ab102cea345b601d9dafb3196358df5448caa88ac3c1e2/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:326f6383fdf2e37ac69773589a8238a3bf396ca8ac8efacb0fb9ed42dd08e426", size = 1418551, upload-time = "2026-10-05T23:24:51.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/6b518a25514f0e643f95610c77e279bfbf0e0b3bd423aac0187d6f039b9a/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d33fc74e43bbd7c3a8f6f7161a8b93b676924286e97e70e828c6e0dcee5c01f", size = 1372130, upload-time = "2026-10-05T23:25:32.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/c2/32538e14e63193ca894ba584696805d1eb45cfc27e15fccd47acfb87531c/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:1994923cf5e5220ae6bf19645302504b27c0289d83e5d8690df71dcae63d8416", size = 1268950, upload-time = "2026-10-05T23:25:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/e50e7e98af9489aa05203c2ab38c95d891dd8d1ed08fad972dcdb6955332/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:501038fd24d3bc95239cfd093a23cf1151f29dd82382a3554dac5dfdab9729ae", size = 1283907, upload-time = "2026-10-05T23:24:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/41c460a7d2148a04b212b2d594d39992fb52e0b844e13bf6784573fc8dea/hypothesis-6.168.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2292ddc24fe6d04b7d30fa6a7e2c9e280ad5078fe671d0bf4aa6df6e143b5ac", size = 1323762, upload-time = "2026-10-05T23:24:18.984Z" }, + { url = "https://files.pythonhosted.org/packages/68/4f/37a7fc1fe445e3589e0f56ff4573c28de1d6e6a03009cba2f99f04e46ffa/hypothesis-6.168.5-cp310-abi3-win32.whl", hash = "sha256:925d67c69b719d416334aa961c0cdfc4a58a471af1ebd2d7101bd515a70f4e5f", size = 677903, upload-time = "2026-10-05T23:25:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/7b25ca7845a60522ebc5f8054f6bba68d47126fb5d940c784fc528a4be4a/hypothesis-6.168.5-cp310-abi3-win_amd64.whl", hash = "sha256:2311590eccba452de863dfe3466daa86a05c25f072ab31ed8bb4d3313ee68439", size = 684616, upload-time = "2026-10-05T23:25:04.028Z" }, + { url = "https://files.pythonhosted.org/packages/c3/00/40e7c36b46c8788eddc7a322ad324e6db53c8ab9a8b9a95d6535ee7bdaaf/hypothesis-6.168.5-cp310-abi3-win_arm64.whl", hash = "sha256:222a6d23a2a824b0f9f73761c2fb9cd2aca96cf3e5b441617625bce4f7eb4fd4", size = 683061, upload-time = "2026-10-05T23:25:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/04/0a/3b3414124055ac49c2478cb49add90eb3b727508b2aa54a4fc50de88f98a/hypothesis-6.168.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8dfead3a6b2e2ceb6165505885b81396b0e3fe8a556bd941d88fa43cd8daff2f", size = 792187, upload-time = "2026-10-05T23:25:51.287Z" }, + { url = "https://files.pythonhosted.org/packages/a1/60/90ccc9e18d831480920dc0f1d33a9af142e796d67dbe6a760e93d0122587/hypothesis-6.168.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:658563b8f2782a0577a4d8d195e31f29b18f3f3b61ba58c4dcbd8e6ac502d14d", size = 788228, upload-time = "2026-10-05T23:24:57.84Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/8257699b8456241b8348fe0071c29912aeeaf5d16ef97a45e9c1d3170ca6/hypothesis-6.168.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f40be9b9c6b7b058ff56b0b18a91ff4cfa57a7c7756043eabaa094a0a162c9", size = 1115527, upload-time = "2026-10-05T23:24:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/42/42/31e66ce21aa6ea030ace8874269e5a169b0c69d8a3043042e315bd64c6ad/hypothesis-6.168.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30208c44364b6fe1f70c74b45f3f1f8a173a749d876294a80fe88c9cf16ab6d0", size = 1157678, upload-time = "2026-10-05T23:25:54.904Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2a/b46ea00cb1cb9930b9cf7f844673913bf8bfc34f38c031d39ede6f649c59/hypothesis-6.168.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09ca5b2f45786feb93ab41c16de602de4a54f42f35985565423417f4ed9d5b6b", size = 1290397, upload-time = "2026-10-05T23:25:34.184Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e8/eb50f72257f8b00f950da99c7ee444aae5f7c6364fce4ffbe82dd550ffdf/hypothesis-6.168.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:257175b2800cb3073f21041d174e67db7613dc64cc79f3f09f93cfecf7cfeb68", size = 1324447, upload-time = "2026-10-05T23:26:32.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/88/cbb53055091323c186752b437024ff6cd95564af4389bfd1b36900aa459d/hypothesis-6.168.5-cp310-cp310-win_amd64.whl", hash = "sha256:3cacf8e84badb92e34336a6b6b95e2135ad248f870382daf56fe471d6c6e794a", size = 684598, upload-time = "2026-10-05T23:24:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/f1149d913d685809c016b2a3ae9d727741ae22f52376c6d0ed51eecb5ac8/hypothesis-6.168.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8c35e5d4a85d0d6071cc267a6cbb8fd7ae23ca8a0f745ea5a52c0064d7c1c4b8", size = 792108, upload-time = "2026-10-05T23:25:12.323Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/7e5ffb6bbfc033c85746243dc4d1541876082e136ee44c02f843bb77427e/hypothesis-6.168.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:244a8d14c0a8a3be0345ad0b120deafb94517cc1d74a961d14b5b5eb041b4c0c", size = 788035, upload-time = "2026-10-05T23:26:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/38/df/022129d3e16d19a84e7a5a35ebf7baca07d3482fb34f0faaab865b14fe66/hypothesis-6.168.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e68e1d43b7c9c7a1aa659dfe1c0ecc2de79391b20db853c1e18ea7e3d2ce31f", size = 1115438, upload-time = "2026-10-05T23:24:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/b3e45b0386d8f643a304105883c5bfce79fd530b2dfe3a70564e1d7aa0bd/hypothesis-6.168.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01a4d3773f285e75551eeef12df058e6316b666bcc3ec187c5eb52a893fbb015", size = 1157503, upload-time = "2026-10-05T23:25:05.609Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4a/aba5a74ddb20c9f41ba5b8f2918c5a12660146cab2120f14122122715060/hypothesis-6.168.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc327005f2fbb55db81d132948ee7c6cec0589694bed04b1e45fc8fc317e12bd", size = 1290235, upload-time = "2026-10-05T23:25:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/34/f4/7204aa6117a38085e6f1dbefd5cd98050a58c847f2bdecc917422cdb2b1c/hypothesis-6.168.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f21c74ad83fe77abc72e82c54114148fb01396769c234e26c9b9dbc21344a9", size = 1324204, upload-time = "2026-10-05T23:26:16.239Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4b/15a46ced6d999148d1b718c5488c243bd56dfcd687a61404fe371192dfd5/hypothesis-6.168.5-cp311-cp311-win_amd64.whl", hash = "sha256:bd3ff6e53e29b86ec6078f123284e65e1c678fe7b30c2b52512244faf266502c", size = 684414, upload-time = "2026-10-05T23:26:18.231Z" }, + { url = "https://files.pythonhosted.org/packages/90/43/a04a727578cbef9f75c11fa6fbad66d13aaffc354f4f979506219814c7d4/hypothesis-6.168.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ddee1ef4bab47e315b705e42d2f4354e789973d11f9620d2df242aef4cfa42b2", size = 793156, upload-time = "2026-10-05T23:25:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/f4/91/55de4e2a12fe98ebd5bc8f35e59870c897ab360cbfe5aa63862cdbef56ad/hypothesis-6.168.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81ceb49b0dc3a4b6126cd0d3bf2b634af4e91513c8f1e2daee16041414ed8e3d", size = 784788, upload-time = "2026-10-05T23:26:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f4/61/230abc6320540bdf73baf9a1c025fb0aa27cfd5a3791a2e0c95114239a70/hypothesis-6.168.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a09caa95d2d7e6546f727f703de606145835d9ca215fb3134a21353c69afaac", size = 1113853, upload-time = "2026-10-05T23:25:30.593Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4d/3bf0a7806b3fa12ed076f2daeb3db0e6f9738994e879432ffd8dbcffd634/hypothesis-6.168.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97ac1d516a42a3b1f13b36a1aa6a5f842e43d67e69d4dc664a9645b28de411ef", size = 1156695, upload-time = "2026-10-05T23:24:28.607Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a0/603f918fcf8f74f81ea593b04e3a9a9fcd426bbf389ed52cb340249bdc14/hypothesis-6.168.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e4819fba78c6cbaa6e2f9fd5a69a413817446943f286763819b5ac52391bff3e", size = 1288792, upload-time = "2026-10-05T23:25:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/69/7c/711ef5be6e889dcd40d9b03cdd85cd42ae39af75835bced3c374730291a9/hypothesis-6.168.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87334b95dfbc101652fa48a427a742b0715b814506d9a10f621c29e476b4a2c1", size = 1323090, upload-time = "2026-10-05T23:25:58.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/0377d7d13ff3e2c16efd141942649edcdb568caec4576f86ac779545dd85/hypothesis-6.168.5-cp312-cp312-win_amd64.whl", hash = "sha256:2fcec23ff4eb526ee85d3510f564b938ca74f6011f1eec1050e4eb55280b0468", size = 681999, upload-time = "2026-10-05T23:25:41.86Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b3/1f7f72cd28d02a5ca99c432fbffe4b750a375df2284af9d916943dd3aa4f/hypothesis-6.168.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:714337b25ca9137bc359c570b868269462307e120999412ca1946f997f4b9db5", size = 793075, upload-time = "2026-10-05T23:25:15.905Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ba/5b0874828695c4d49e3858d0967254f783e563cd0e211a6db27d11d48a1f/hypothesis-6.168.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f1c3617155fcf5b5259a1f2e4c775d3eec7bfa80b162b2f6f145b08f871ab08", size = 784693, upload-time = "2026-10-05T23:24:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/ca777becba5251b0d778bb9d83d15524c559a07e4b5d4e6211473855bae2/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebee70b7a026210bb47c86c89e5bfb42effd5bd630080e76bc084f29c01c7f7a", size = 1113693, upload-time = "2026-10-05T23:24:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/5a74bf329e405db3edc5639a2595eccf33ad6f5aaa191019e9f824d630f4/hypothesis-6.168.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cfb06b31cca005345b8ad63f88986d21fd359a7dc3dba2965dd3515b720e5c9", size = 1156530, upload-time = "2026-10-05T23:24:47.153Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/e79cf67f03f212a1394abac21053bd6887aa70f557be1da3f9c9c73e58ae/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a4c244d7ab64963fb575f0ec2d813630e1d14cefc39e7c460d5d778e5af4118", size = 1288713, upload-time = "2026-10-05T23:25:22.763Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/df452159ac8d7b278071a3e81fafc69da833ec4302b8c85f5b6e530aea21/hypothesis-6.168.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e59d519f6fb38b3fa4fcde046767b03a24740fe827d261ee7ff9a721c06169b", size = 1323134, upload-time = "2026-10-05T23:26:02.485Z" }, + { url = "https://files.pythonhosted.org/packages/af/fb/f07d8d09fb57eb14555cad64dfbe29bfdcecff3806f1e01268258088e741/hypothesis-6.168.5-cp313-cp313-win_amd64.whl", hash = "sha256:c103f655644afa4ef6bf7efbf86e44b78ee475fd0691da2db86e2cfe72c07234", size = 682019, upload-time = "2026-10-05T23:24:22.888Z" }, + { url = "https://files.pythonhosted.org/packages/de/e9/7c3c2262b8cfa825c4c1764d62aa15e628bae257ccfd2ee4f3ffa4f81eaa/hypothesis-6.168.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c4dc037d8001bc6eccb8636f4a38d16ea6b250d6bf0a89075aaa5e5069f751cc", size = 793253, upload-time = "2026-10-05T23:26:14.331Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/7909ed7d29302491e9b7bc0e7ac3287c20736c05a0cc35bae65024aeec3b/hypothesis-6.168.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c90743321f29b65491d146adfc2ece85869bacb71ce18b47674795e896c81ee3", size = 784837, upload-time = "2026-10-05T23:25:24.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/57742c459349052e6a3e0c011855840f8cbbbadca91079d5b591f08b25ae/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09debb7f7f0f229da5f7e2ad515a5be7a8dc607ec204074775f8ab6731a447f0", size = 1114268, upload-time = "2026-10-05T23:24:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/80/07bd2449f91f9426f705fb689429bab6e26d1365f8ac4ef7d7c1cec9055e/hypothesis-6.168.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d227f8ac497eca0bde4e8562d32dd4e82fc9566526020bbd567f76b833b923b0", size = 1156838, upload-time = "2026-10-05T23:24:35.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/75/7f3dda517e5134f73e2ae41821bf40b3fd3ac6551a9a43ea9287471738a1/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc6ebd35601c72c842e5899c3f760f9ed26c69e786ee40a9a64fb5a4a3058315", size = 1288864, upload-time = "2026-10-05T23:24:42.645Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cd/4b1364140642cf3f1431ca59b5841fc322872dfa7197b2facb97692da234/hypothesis-6.168.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:503e103ad49e702bad200157d82778eebbc14d3045e9700a8e8fe5db40912953", size = 1323300, upload-time = "2026-10-05T23:24:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/47d7cffcaf15318a4308516b6b3d2fd0db599f18eacc0f2dc553be2206a7/hypothesis-6.168.5-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bc5cc310f9f86ec62f0d0dd7eea5a4788f18ec793b70ee2c7163b916768e1057", size = 624239, upload-time = "2026-10-05T23:24:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/97/6e/2ca0f68150be175b7cfa7bfb6692260638d86aeb9313478ba82e198186e6/hypothesis-6.168.5-cp314-cp314-win_amd64.whl", hash = "sha256:71ce0599e806ce3a68f9f118edf450bf091e11b134f6bcc5f8dd706b42c91ebc", size = 681884, upload-time = "2026-10-05T23:25:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/4fc2b5970df0c27668ec08abc505f1d01314a69953f89dc0edc6528ff5a0/hypothesis-6.168.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f66b02c9e95e916a2c58f725a92377ec988146ed7b5aeccd5e78ceecac1eae6f", size = 791636, upload-time = "2026-10-05T23:26:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/03cdf5f052dcb441e045be1fd0aa531e85cde1a1cbabab60968625c570a3/hypothesis-6.168.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bab27926e1d1575fb43b70d4aeece05b74a5e477af0509b56cb6fd778070dd93", size = 783280, upload-time = "2026-10-05T23:26:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/615557af244e2f3ce4763029c03a62ed82dcbbd646b72a8c479ef0408b33/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edeb42c3009b5652dc1c44907ec91bfe9284100ad5e57993dfebabb76f2961a1", size = 1112149, upload-time = "2026-10-05T23:24:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/67/a6707fcd51dc5f2531bf88ac072e99f31ab9d8020488b01349a6d2981081/hypothesis-6.168.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8977456328147c521a16a089325017b2c728fddc23351693a4fd924cc7fc7001", size = 1155637, upload-time = "2026-10-05T23:24:24.047Z" }, + { url = "https://files.pythonhosted.org/packages/85/d4/ac2e852d2f163afd398854662bcbb0b849a767abbf2f95a75de6f685f821/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:36ecf7ac351f9c0b5489ba800884b607da754e88ef40713fbfcc170d2151e6eb", size = 1287040, upload-time = "2026-10-05T23:25:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/f77b1602704bda6ff3d9d0817120bd7fb94fd792bd25508b36ce4b8bd2a2/hypothesis-6.168.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0333aa5129ba3019a83fb81a7f0fc238180e415a9edddd9a15101f8deaaa517e", size = 1322166, upload-time = "2026-10-05T23:25:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2e/94138a73e0906b31cb5968d20be58688f582a09e5958f2c75d45a7049545/hypothesis-6.168.5-cp314-cp314t-win_amd64.whl", hash = "sha256:2fcb87341d76ae0183e8219c9a14d55957c50d14973879db5fea3e81da45ba1a", size = 681599, upload-time = "2026-10-05T23:24:37.827Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/92cb8092b680be2b6ec5ffe83b9f1a98dbf414117566f9e3ba4e8b569214/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:453ab7d0a1fadbaa54ae8722d22463cc2046fa8ef25b9b88715d28279bf79fc1", size = 790950, upload-time = "2026-10-05T23:24:45.852Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/78aea12694e3d1177e2980d44798b6d93e191faf59155b18bf5ae315f6a2/hypothesis-6.168.5-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:bbdbc43d1f9dad595b249b7bbe8ee5102bc94a4fcb0a79ff76d20e41fcfe342a", size = 782939, upload-time = "2026-10-05T23:25:39.961Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/5ef9947b2d149f773428e555bdf66688405aa5167510bdbe97c8ec5c6090/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bc36194d7b6083591060836c7872711a6820217b325bf432dd7e10b3d4af5cb", size = 1111380, upload-time = "2026-10-05T23:24:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/e1/65/7e668e203fb2659c6214dc0c24cc09b7dea8a02c7c8d0ad338f644a054c4/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22425e2b1543a43c157a81472c713ba8f291cbaf054c70ffe128e2cacc294f65", size = 1142644, upload-time = "2026-10-05T23:26:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/b112978676e795658d58c4294bf90cdbb8cb56cb8292c8c4874650468cf9/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea0bc513d0e38d1d5ddfb581132928871cd02dc54dfe4511a5396727c48e9d0", size = 1135966, upload-time = "2026-10-05T23:24:49.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/27/cd3bf01e8246c4318ec3df15f5eeeee3214f444df0129a6c7f9a62859ee8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb142bc70bbf6645e15c7ca72de3f7c8dae198aa2743a609f4f3e3bb4f9c3a52", size = 1189646, upload-time = "2026-10-05T23:26:04.471Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/4d3e882f31c289e432dfec34dbb9029296038a8c69e8b28cebb0a5fb7ea8/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a27b758707bd37f5a1759cca6eef83fe1a212c38dc4ca0a203434004c5647d15", size = 1154770, upload-time = "2026-10-05T23:25:10.814Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/96f5455e1b3d0409cbbb1434c98e792bcceefd614a4b11e600072520b487/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:77a111cb50c330fa7098f65852fa17a01ecd781a85be3cf5e5871bdeeeb0ecbc", size = 1109925, upload-time = "2026-10-05T23:25:26.369Z" }, + { url = "https://files.pythonhosted.org/packages/3d/64/0758985d9d36f0c5ec981a1457ea1c8173f62d46a94531417aec117df4d7/hypothesis-6.168.5-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdd0afc13e86ec76cae3d3659569c1f601f4e9ca52b5cf91c1685979eae64d7b", size = 1148476, upload-time = "2026-10-05T23:24:54.552Z" }, + { url = "https://files.pythonhosted.org/packages/43/5c/a9b8953e1d8aefcd3c22cf8d10dd8acf93e602b903278e2e51cf8544ccea/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:5fefb02035864c3d322e3b0969b296250923fdcfb574ea1ad4374f1a6333f663", size = 1285856, upload-time = "2026-10-05T23:25:38.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/66098444dc832523ddc4f2e05723662834e5f99bba3c759615d059f6420e/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:9db8aa1f5529e1b577ec18b775c2fb4225821712e946f7762b90c966604faf83", size = 1415500, upload-time = "2026-10-05T23:24:21.682Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/e971b6fe20ef8d7c2019cbf24b4f6149468efc88c42a744f5bc99e6ca0ed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:59e07d2f62b5ff573b0059959ae9cef9edfb0f5393fdb35ea81fce1ee77b27ac", size = 1368623, upload-time = "2026-10-05T23:26:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b279dfbd2c06cdb3030ba7eea042cb2cf0171d0013563d103d5207dde63b/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a03ca128bea29d6826fc545f1f6289fb1ea2e83a5bb811321761b2d515ca575", size = 1264457, upload-time = "2026-10-05T23:25:21.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/16ac9f8ddfada1cd278bd2185234d0d36ebd304926b69ad0497c210c6fed/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:5c03f2d3f84f626f3fd07f54573ab40455e1a1996e98a4f4971caf8b7e796afe", size = 1281744, upload-time = "2026-10-05T23:24:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/99a44430b82998fdef0ffd7d353f64ee5f078c2805ff70f8677ee102cb6c/hypothesis-6.168.5-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2bdf8ce9b72a620cd5ec4dd6b1c1837ff6971489a863851d11d9b0f58dd4062a", size = 1321504, upload-time = "2026-10-05T23:26:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/58ef2564d1985a5c1a1dc57906b8363a767094abca180e80a0aca4cb635f/hypothesis-6.168.5-cp315-abi3.abi3t-win32.whl", hash = "sha256:5c3abbef7b17571fd713b0922407d9cd8cbc652254c0f462875f15199fcb29f7", size = 674959, upload-time = "2026-10-05T23:24:36.482Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/153414f55eb0c85bd9d891bd7811d746978c7ad3de81ea79eeb4e62e088b/hypothesis-6.168.5-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:38172199abab94a04bc017613e055faa796d7175fbc6221aac504d406c960b60", size = 681329, upload-time = "2026-10-05T23:26:08.897Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/117c82f08ab3ba1dcfbf6562ac43b8deb8efa8106646494fadd15122cc1b/hypothesis-6.168.5-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:0600ddc24c32dab5ca8e780630ab6e2561df6d7f594f781d0608b38e04c4da91", size = 679632, upload-time = "2026-10-05T23:24:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/73/25/5c38b739fb778d4de48aab6509b9cf0afd0317bb0459741afdcd0ad44aed/hypothesis-6.168.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6786049db92275e0c5cfac7dfcda6d4bbc80bdf84cbc8c9c7171ca17f47b5aac", size = 792028, upload-time = "2026-10-05T23:24:56.365Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3f/91071d53240f5f13ab1dda286e3ddb33177537dbf55cede76e7f4a3856db/hypothesis-6.168.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ffbde24430dcd73231fd03324a934e0f638f7c0899fc566f3ef8c851534f8030", size = 787324, upload-time = "2026-10-05T23:24:17.822Z" }, + { url = "https://files.pythonhosted.org/packages/10/ef/eb262e50d7741de6c49d27923e2c282d079273b8bcdacde33165ea39488d/hypothesis-6.168.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea967baaedfd532f1a521aaedafc66bb9de09795071492b0e7252139df38479f", size = 1115118, upload-time = "2026-10-05T23:24:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/87/67/a655a8666164aa896516f919af272fa3a3a00d2786be880c31bb638e79e2/hypothesis-6.168.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f98289a5da876c08b9eeb68d1cfdfbd0fcc110cf364d33c3cc32cf229ffe8", size = 1157894, upload-time = "2026-10-05T23:26:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/57/4d/71c422a29446c03e9a052f10b8ee527044242e71e3c0139100991f721e16/hypothesis-6.168.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e313a01ce580180dc3bb8fa98ddd0ffb20e51e108d9fa747ba6c1596790dc3fa", size = 685030, upload-time = "2026-10-05T23:24:09.964Z" }, ] [[package]] @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.5" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -234,125 +234,156 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.5" +version = "2.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, - { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, - { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, - { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, - { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, - { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, - { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, - { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, - { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, - { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, - { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, - { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, - { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, - { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, - { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, - { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, - { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, - { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, - { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, - { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, - { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, - { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, - { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, - { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, - { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, - { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, - { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, - { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, - { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, - { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, - { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, - { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, - { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, - { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, - { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, - { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, - { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, - { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, - { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, - { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, + { url = "https://files.pythonhosted.org/packages/09/db/c9cc34707ba2c23ce90d54e8a4ee9307583ad054b9df2ce570ba550cb576/pydantic_core-2.50.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b281a3b0f0822618fe5e3e0d8a2048b6356b14388505dc9374ccffeb69989713", size = 2154222, upload-time = "2026-10-11T18:31:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/6963f4ee55b7df7658f5c036c0f90baf8cd3d413f5d8b551f227e8467afc/pydantic_core-2.50.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa4c8bc12c1354c5550c0c35c1852c8c1901e89e06561724e03f8d0342e1f87", size = 2048126, upload-time = "2026-10-11T18:31:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/bf/be/f75637e222085d29a3076edbc5cfd0dd9b77d145e310ff7dc1150f07558a/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa9de446b793de2beb6fa2d9d0961803126c4e2a99c2f25ab59b9fd6ea125c0", size = 2052460, upload-time = "2026-10-11T18:31:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/3d192ffa39ce3bd0e98e988fb5c055cfbf4af6e6e155a4ca5baa86d1f2cc/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de531ce1e2a3364e8767878b58f4ff728a434b4fde089781fe30b1e08e2396e0", size = 2190750, upload-time = "2026-10-11T18:31:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/12ea94df5220ab316ee5bb498ebcf710cff2db352653039fb6033f6997d1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c58106de36ac6a56314182958de20db8d3a29dfd5db527192cc754e4f8e7fb", size = 2390043, upload-time = "2026-10-11T18:31:44.745Z" }, + { url = "https://files.pythonhosted.org/packages/f6/bc/4fbb6dc90af038628ec824434a9841be9d46854f9267bb658f09e315024d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b4c3df25bd323bf1d36a648d563cf1fc69d717451569927151bdad7cad07a77", size = 2432397, upload-time = "2026-10-11T18:31:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1a/21a58791021397d9ca35295e3f4291c16e319155c5904334b89eb07e97e1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77ac30b19221cd9bd3fcfa3d4614eff93140d0572ab730cded17b64adca05f3", size = 2160719, upload-time = "2026-10-11T18:31:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/d5b2c5876162fd2c444eaed0064b5d9d9d0ef0d31530fc6a7997e8685ad8/pydantic_core-2.50.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d939de9c82e2126f7f48a7e658f8a85ed46d57662d53f44c49b8895fe94a3eb7", size = 2256164, upload-time = "2026-10-11T18:31:48.797Z" }, + { url = "https://files.pythonhosted.org/packages/af/e8/1a6625d6e99250388dd0ae817c5d75839cd6103e7b790f59e0ffff74955d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:30ddf019d082c117b5d309e5b86710c2a78909907ec1a9381feec3eec02eca0b", size = 2271176, upload-time = "2026-10-11T18:31:49.987Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/bbdd137d7ec41284701dab4d1489ea597d401dff2f5f3e6b41ea872e0b12/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ab756b72bd5054e4c7ef3ded331b35786cbd3cf931531a508f79a9537517064", size = 2304637, upload-time = "2026-10-11T18:31:51.251Z" }, + { url = "https://files.pythonhosted.org/packages/49/5d/7bd322e649b41b45130bc00bfa34dc57498c853aa8e6fc9604e33bc7087c/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:b087b1c5be7ac687cf22eabfe4b6b608d40df23610651e93611e1f49118baf84", size = 2467374, upload-time = "2026-10-11T18:31:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/53/48/41a91111590eb7ba68a7c64c03139235a4b8349930abcb87931aa80da796/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a44101320cfe99432db74237545a63057dc7a88dfe792cbcad0647f2af56cb81", size = 2480832, upload-time = "2026-10-11T18:31:54.06Z" }, + { url = "https://files.pythonhosted.org/packages/2a/02/4ac93a3d3f0ff412df1a36a9619743336fe6eec68c0df8a914163a3d22b4/pydantic_core-2.50.1-cp310-cp310-win32.whl", hash = "sha256:a4aaaa791bdae1c972a7e81765f4f3571c926b8e0b9b6e47346499fb80079665", size = 2123154, upload-time = "2026-10-11T18:31:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ba/3a7af3dcd480c4739aa82cd9fedee79a5531a7c553346abcf095bd05f0de/pydantic_core-2.50.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eedf82ee4753cdab8e50044c6bd569577eebc3859b11fecf4eb9223761ff966", size = 2179366, upload-time = "2026-10-11T18:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, + { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, ] [[package]] @@ -482,12 +513,12 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] From 54b830935b6127db1a01fb5b91561d80114ab99c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:29:46 +0200 Subject: [PATCH 164/322] chore: Bump the all-actions group with 4 updates (#5066) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/publish-pypi.yml | 12 +++++------ .github/workflows/repo-assist.lock.yml | 26 +++++++++++------------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 4319e2cf67..bdd7390d07 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup-cli@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: version: v0.71.5 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 178d5f3ee7..39f4d30d07 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -51,7 +51,7 @@ jobs: mkdir -p fable_library echo "__version__ = \"$VERSION\"" > fable_library/_version.py - name: Upload fable-library-py - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: fable-library-py path: ./temp/fable-library-py @@ -100,7 +100,7 @@ jobs: manylinux: auto working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-linux-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -139,7 +139,7 @@ jobs: manylinux: musllinux_1_2 working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-musllinux-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -174,7 +174,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-windows-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -209,7 +209,7 @@ jobs: sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} working-directory: ./temp/fable-library-py - name: Upload wheels - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-macos-${{ matrix.platform.target }} path: ./temp/fable-library-py/dist @@ -232,7 +232,7 @@ jobs: args: --out dist working-directory: ./temp/fable-library-py - name: Upload sdist - uses: actions/upload-artifact@v7.0.1 + uses: actions/upload-artifact@v7.0.2 with: name: wheels-sdist path: ./temp/fable-library-py/dist diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index bf5d9b5b20..d5ff7a08fe 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -419,7 +419,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: activation include-hidden-files: true @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1299,7 +1299,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/sanitize_repo_memory_filenames.sh" - name: Upload repo-memory artifact (default) if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: repo-memory-default path: /tmp/gh-aw/repo-memory/default @@ -1308,7 +1308,7 @@ jobs: - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: agent path: | @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1648,7 +1648,7 @@ jobs: mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: '24' package-manager-cache: false @@ -1697,7 +1697,7 @@ jobs: XDG_CONFIG_HOME: /home/runner - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8a5b99ba734d5cb564e3a0c85ea0580d881002e0 # v0.90.3 + uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -2004,7 +2004,7 @@ jobs: echo "Token invalidation step complete." - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: safe-outputs-items path: | From dbbbab942a5165649266f2336c67616f8231a0ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:33:42 +0200 Subject: [PATCH 165/322] chore: Bump the all-nuget group with 17 updates (#5070) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 6 +++--- src/Directory.Build.props | 2 +- src/Fable.Analyzers/Fable.Analyzers.fsproj | 2 +- src/Fable.Build/Fable.Build.fsproj | 4 ++-- src/Fable.Cli/Fable.Cli.fsproj | 2 +- src/Fable.Compiler/Fable.Compiler.fsproj | 6 +++--- src/fable-standalone/src/Fable.Standalone.fsproj | 4 ++-- src/fable-standalone/src/Worker/Worker.fsproj | 4 ++-- tests/Integration/Compiler/Fable.Tests.Compiler.fsproj | 2 +- .../Integration/Integration/Fable.Tests.Integration.fsproj | 2 +- tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj | 2 +- tests/Js/Main/Fable.Tests.fsproj | 2 +- tests/Python/Fable.Tests.Python.fsproj | 4 ++-- tests/React/Fable.Tests.React.fsproj | 2 +- tests/Rust/Fable.Tests.Rust.fsproj | 2 +- 15 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a6f5d34e4c..62e8ef1102 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.7", + "version": "8.0.8", "commands": [ "fantomas" ], @@ -17,14 +17,14 @@ "rollForward": false }, "fsharp-analyzers": { - "version": "0.39.2", + "version": "0.40.0", "commands": [ "fsharp-analyzers" ], "rollForward": false }, "easybuild.shipit": { - "version": "3.1.0", + "version": "3.2.0", "commands": [ "shipit" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d5525ec703..62019bf3f9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,7 +23,7 @@ - + all build diff --git a/src/Fable.Analyzers/Fable.Analyzers.fsproj b/src/Fable.Analyzers/Fable.Analyzers.fsproj index 26837db080..dec98bff99 100644 --- a/src/Fable.Analyzers/Fable.Analyzers.fsproj +++ b/src/Fable.Analyzers/Fable.Analyzers.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index 944c97afe6..326ea23a0e 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -47,9 +47,9 @@ - + - + diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index c5340e3a94..2f84bcd80d 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -53,7 +53,7 @@ all - + diff --git a/src/Fable.Compiler/Fable.Compiler.fsproj b/src/Fable.Compiler/Fable.Compiler.fsproj index 0e15e7a1c5..fc0b532aca 100644 --- a/src/Fable.Compiler/Fable.Compiler.fsproj +++ b/src/Fable.Compiler/Fable.Compiler.fsproj @@ -41,9 +41,9 @@ all - - - + + + diff --git a/src/fable-standalone/src/Fable.Standalone.fsproj b/src/fable-standalone/src/Fable.Standalone.fsproj index ec13e56eab..97dac79b02 100644 --- a/src/fable-standalone/src/Fable.Standalone.fsproj +++ b/src/fable-standalone/src/Fable.Standalone.fsproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/fable-standalone/src/Worker/Worker.fsproj b/src/fable-standalone/src/Worker/Worker.fsproj index a86190c262..a9ce5f3a3a 100644 --- a/src/fable-standalone/src/Worker/Worker.fsproj +++ b/src/fable-standalone/src/Worker/Worker.fsproj @@ -11,7 +11,7 @@ - - + + diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index d25012ac7c..8a2b09954d 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -7,7 +7,7 @@ $(DefineConstants);DOTNET_FILE_SYSTEM - + diff --git a/tests/Integration/Integration/Fable.Tests.Integration.fsproj b/tests/Integration/Integration/Fable.Tests.Integration.fsproj index a87aacd988..56465bdd44 100644 --- a/tests/Integration/Integration/Fable.Tests.Integration.fsproj +++ b/tests/Integration/Integration/Fable.Tests.Integration.fsproj @@ -6,7 +6,7 @@ Major - + diff --git a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj index 7c31f7d3ed..d5485bf1af 100644 --- a/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj +++ b/tests/Js/Adaptive/Fable.Tests.Adaptive.fsproj @@ -11,6 +11,6 @@ - + diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 99d1843057..ce2d68b963 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -11,7 +11,7 @@ - + diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 92bfcf5106..b5f130a704 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -13,7 +13,7 @@ Fable.Tests - + @@ -21,7 +21,7 @@ all - + diff --git a/tests/React/Fable.Tests.React.fsproj b/tests/React/Fable.Tests.React.fsproj index cfee412f9a..c86cba07eb 100644 --- a/tests/React/Fable.Tests.React.fsproj +++ b/tests/React/Fable.Tests.React.fsproj @@ -28,7 +28,7 @@ - + diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index dfd8096cf8..74b5b52f70 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -12,7 +12,7 @@ Preview - + From e38e2f673276679c157a2f2cb9a26e3c4739b286 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:40:53 +0200 Subject: [PATCH 166/322] chore: Bump the all-pip group with 3 updates (#5068) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 6 +- uv.lock | 257 +++++++++++++++++++++++++++---------------------- 2 files changed, 147 insertions(+), 116 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5f63cbdb5d..bab49efc12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.16.10,<0.17", + "ruff>=0.17.0,<0.18", "maturin>=1.15.0,<2", "dunamai>=1.26.2,<2", - "pydantic>=2.13.5", + "pydantic>=2.14.1", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.84", + "ty>=0.0.86", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 445b7b96b9..e9193f5bc0 100644 --- a/uv.lock +++ b/uv.lock @@ -65,11 +65,11 @@ dev = [ { name = "attrs", specifier = ">=26.1.0" }, { name = "dunamai", specifier = ">=1.26.2,<2" }, { name = "maturin", specifier = ">=1.15.0,<2" }, - { name = "pydantic", specifier = ">=2.13.5" }, + { name = "pydantic", specifier = ">=2.14.1" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.16.10,<0.17" }, - { name = "ty", specifier = ">=0.0.84" }, + { name = "ruff", specifier = ">=0.17.0,<0.18" }, + { name = "ty", specifier = ">=0.0.86" }, ] [[package]] @@ -131,7 +131,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.5" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -139,84 +139,115 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.5" +version = "2.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, - { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, - { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, - { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, - { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, - { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, - { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, - { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, - { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, - { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, - { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, - { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, - { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, - { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, - { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, - { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, - { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, - { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, - { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, ] [[package]] @@ -259,52 +290,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.10" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, - { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, - { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, - { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, - { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, + { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, + { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, + { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, + { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, ] [[package]] name = "ty" -version = "0.0.84" +version = "0.0.86" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, - { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, - { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, - { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, - { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, + { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, ] [[package]] @@ -318,12 +349,12 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] From cbb255cd7a7082af3987c4dacdb9935963bbfee2 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 14 Oct 2026 22:43:41 +0200 Subject: [PATCH 167/322] chore: enforce conventional Dependabot titles (#5071) --- .github/dependabot.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f5690d2a79..4e896361fe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,6 +2,8 @@ version: 2 updates: - package-ecosystem: "npm" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -18,6 +20,8 @@ updates: - package-ecosystem: "nuget" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -34,6 +38,8 @@ updates: - package-ecosystem: "github-actions" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 10 @@ -46,6 +52,8 @@ updates: - package-ecosystem: "uv" directory: "/" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -58,6 +66,8 @@ updates: - package-ecosystem: "uv" directory: "/src/fable-library-py" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -70,6 +80,8 @@ updates: - package-ecosystem: "npm" directory: "/tests/React" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -94,6 +106,8 @@ updates: - package-ecosystem: "cargo" directory: "/src/fable-library-rust" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -106,6 +120,8 @@ updates: - package-ecosystem: "cargo" directory: "/src/fable-library-py" + commit-message: + prefix: "chore" schedule: interval: "weekly" open-pull-requests-limit: 5 From 2ea792c56d1cbdfce7666e4ebcf59edce2d2dcc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:05:15 +0200 Subject: [PATCH 168/322] chore: Bump the all-npm group across 1 directory with 3 updates (#5065) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 232 +++++++++++++++++++++++----------------------- package.json | 6 +- 2 files changed, 119 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73056adc0c..231f9c3679 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.6.4", - "concurrently": "^10.0.5", + "@types/node": "^26.6.5", + "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.0", + "rollup": "^4.64.5", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", - "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", + "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", - "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", + "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", - "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", + "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", - "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", + "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", - "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", + "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", - "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", + "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", - "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", + "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", - "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", + "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", - "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", + "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", - "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", + "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", - "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", + "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", - "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", + "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", - "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", + "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", - "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", + "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", - "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", + "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", - "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", + "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", - "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", + "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", - "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", + "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", - "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", + "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", - "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", + "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", - "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", + "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", - "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", + "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", - "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", + "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", - "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", + "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", - "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", + "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", "cpu": [ "x64" ], @@ -482,9 +482,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", - "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -1050,14 +1050,14 @@ } }, "node_modules/concurrently": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", - "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.6.tgz", + "integrity": "sha512-qgICXXFp2/3nAebRLmxcINKEXM1mLK3ij/y31P02VTs/KIkddIeC05n73DxOXsXMB9LFSqNU1/na3lDlOhcbIw==", "license": "MIT", "dependencies": { "chalk": "5.6.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "10.2.2", "tree-kill": "1.2.2", "yargs": "18.0.0" @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.64.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz", - "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", + "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.64.0", - "@rollup/rollup-android-arm64": "4.64.0", - "@rollup/rollup-darwin-arm64": "4.64.0", - "@rollup/rollup-darwin-x64": "4.64.0", - "@rollup/rollup-freebsd-arm64": "4.64.0", - "@rollup/rollup-freebsd-x64": "4.64.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", - "@rollup/rollup-linux-arm-musleabihf": "4.64.0", - "@rollup/rollup-linux-arm64-gnu": "4.64.0", - "@rollup/rollup-linux-arm64-musl": "4.64.0", - "@rollup/rollup-linux-loong64-gnu": "4.64.0", - "@rollup/rollup-linux-loong64-musl": "4.64.0", - "@rollup/rollup-linux-ppc64-gnu": "4.64.0", - "@rollup/rollup-linux-ppc64-musl": "4.64.0", - "@rollup/rollup-linux-riscv64-gnu": "4.64.0", - "@rollup/rollup-linux-riscv64-musl": "4.64.0", - "@rollup/rollup-linux-s390x-gnu": "4.64.0", - "@rollup/rollup-linux-x64-gnu": "4.64.0", - "@rollup/rollup-linux-x64-musl": "4.64.0", - "@rollup/rollup-openbsd-x64": "4.64.0", - "@rollup/rollup-openharmony-arm64": "4.64.0", - "@rollup/rollup-win32-arm64-msvc": "4.64.0", - "@rollup/rollup-win32-ia32-msvc": "4.64.0", - "@rollup/rollup-win32-x64-gnu": "4.64.0", - "@rollup/rollup-win32-x64-msvc": "4.64.0", + "@rollup/rollup-android-arm-eabi": "4.64.5", + "@rollup/rollup-android-arm64": "4.64.5", + "@rollup/rollup-darwin-arm64": "4.64.5", + "@rollup/rollup-darwin-x64": "4.64.5", + "@rollup/rollup-freebsd-arm64": "4.64.5", + "@rollup/rollup-freebsd-x64": "4.64.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", + "@rollup/rollup-linux-arm-musleabihf": "4.64.5", + "@rollup/rollup-linux-arm64-gnu": "4.64.5", + "@rollup/rollup-linux-arm64-musl": "4.64.5", + "@rollup/rollup-linux-loong64-gnu": "4.64.5", + "@rollup/rollup-linux-loong64-musl": "4.64.5", + "@rollup/rollup-linux-ppc64-gnu": "4.64.5", + "@rollup/rollup-linux-ppc64-musl": "4.64.5", + "@rollup/rollup-linux-riscv64-gnu": "4.64.5", + "@rollup/rollup-linux-riscv64-musl": "4.64.5", + "@rollup/rollup-linux-s390x-gnu": "4.64.5", + "@rollup/rollup-linux-x64-gnu": "4.64.5", + "@rollup/rollup-linux-x64-musl": "4.64.5", + "@rollup/rollup-openbsd-x64": "4.64.5", + "@rollup/rollup-openharmony-arm64": "4.64.5", + "@rollup/rollup-win32-arm64-msvc": "4.64.5", + "@rollup/rollup-win32-ia32-msvc": "4.64.5", + "@rollup/rollup-win32-x64-gnu": "4.64.5", + "@rollup/rollup-win32-x64-msvc": "4.64.5", "fsevents": "~2.3.2" } }, @@ -2432,9 +2432,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "license": "MIT", "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index b8b6790ce3..f84c42b833 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.6.4", - "concurrently": "^10.0.5", + "@types/node": "^26.6.5", + "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.0", + "rollup": "^4.64.5", "terser": "^5.51.2", "typescript": "^7.0.2" }, From e6b72ec73be454bd3b3233167542a65fc6553c8a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 14 Oct 2026 23:15:16 +0200 Subject: [PATCH 169/322] fix(js/ts): avoid full array copy in Encoding.GetString (#5069) --- src/fable-library-ts/Encoding.ts | 35 +++++++++++++++++++------------- tests/Js/Main/ConvertTests.fs | 17 ++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/fable-library-ts/Encoding.ts b/src/fable-library-ts/Encoding.ts index 781a920d2c..37495c8f3a 100644 --- a/src/fable-library-ts/Encoding.ts +++ b/src/fable-library-ts/Encoding.ts @@ -89,6 +89,25 @@ function utf8_decode(bytes: ArrayLike) { return chars.join(""); } +function toUint8Array(bytes: ArrayLike, index?: number, count?: number) { + if (ArrayBuffer.isView(bytes)) { + const buffer = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength); + if (index != null && count != null) { + return buffer.subarray(index, index + count); + } else if (index != null) { + return buffer.subarray(index); + } + return buffer; + } else { + if (index != null && count != null) { + return Uint8Array.from(Array.prototype.slice.call(bytes, index, index + count)); + } else if (index != null) { + return Uint8Array.from(Array.prototype.slice.call(bytes, index)); + } + return Uint8Array.from(bytes); + } +} + class UTF16LE { public getBytes(str: string | string[], index?: number, count?: number) { @@ -110,13 +129,7 @@ class UTF16LE { } public getString(bytes: ArrayLike, index?: number, count?: number) { - const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes); - let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); - if (index != null && count != null) { - buffer = buffer.subarray(index, index + count); - } else if (index != null) { - buffer = buffer.subarray(index); - } + const buffer = toUint8Array(bytes, index, count); // Allow to compile for Browser environment without @types/node being installed // See https://github.com/fable-compiler/Fable/issues/4368 const g = globalThis as any; @@ -154,13 +167,7 @@ class UTF8 { } public getString(bytes: ArrayLike, index?: number, count?: number) { - const array = ArrayBuffer.isView(bytes) ? bytes : Uint8Array.from(bytes); - let buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); - if (index != null && count != null) { - buffer = buffer.subarray(index, index + count); - } else if (index != null) { - buffer = buffer.subarray(index); - } + const buffer = toUint8Array(bytes, index, count); // Allow to compile for Browser environment without @types/node being installed // See https://github.com/fable-compiler/Fable/issues/4368 const g = globalThis as any; diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 171ada44ad..93dd760e84 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -26,6 +26,13 @@ let parse f (a: string) = #endif +let private toPlainArray (bytes: byte[]) : byte[] = +#if FABLE_COMPILER + ResizeArray bytes |> unbox +#else + bytes +#endif + let private adaptExponentValue (value: string) = value.ToLowerInvariant().Replace("e+00", "e+").Replace("e+0", "e+") @@ -2406,6 +2413,11 @@ let tests = System.Text.Encoding.Unicode.GetString(bytes, 8, 6) |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.Unicode.GetString for range works with non-typed array" <| fun () -> + let bytes = toPlainArray [| 0x7Auy; 0x00uy; 0x61uy; 0x00uy; 0x06uy; 0x03uy; 0xFDuy; 0x01uy; 0xB2uy; 0x03uy; 0xFFuy; 0xD8uy; 0xFFuy; 0xDCuy |] + System.Text.Encoding.Unicode.GetString(bytes, 8, 6) + |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.UTF8.GetBytes works" <| fun () -> System.Text.Encoding.UTF8.GetBytes("za\u0306\u01FD\u03B2\uD8FF\uDCFF") |> equal [| 0x7Auy; 0x61uy; 0xCCuy; 0x86uy; 0xC7uy; 0xBDuy; 0xCEuy; 0xB2uy; 0xF1uy; 0x8Fuy; 0xB3uy; 0xBFuy |] @@ -2424,4 +2436,9 @@ let tests = System.Text.Encoding.UTF8.GetString(bytes, 6, 6) |> equal "\u03B2\uD8FF\uDCFF" + testCase "Encoding.UTF8.GetString for range works with non-typed array" <| fun () -> + let bytes = toPlainArray [| 0x7Auy; 0x61uy; 0xCCuy; 0x86uy; 0xC7uy; 0xBDuy; 0xCEuy; 0xB2uy; 0xF1uy; 0x8Fuy; 0xB3uy; 0xBFuy |] + System.Text.Encoding.UTF8.GetString(bytes, 6, 6) + |> equal "\u03B2\uD8FF\uDCFF" + ] From 5fa673dbfe5bd2df4b7093ce2df50c1aef0853ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:15:25 +0200 Subject: [PATCH 170/322] chore: Bump the all-nuget group with 1 update (#5072) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 62e8ef1102..15b2c543a5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.8", + "version": "8.0.10", "commands": [ "fantomas" ], From 9bde2e41d8483100ed14bbb37fa261896a6c88a4 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 09:02:36 +0200 Subject: [PATCH 171/322] perf(all): enable server GC and immediate JIT tiering (#5073) --- src/Fable.Cli/Fable.Cli.fsproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index 2f84bcd80d..d1e50711e2 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -12,7 +12,12 @@ true F# to JS compiler $(OtherFlags) --nowarn:3536 + true + + + + From d42aa6b926ac531aba7069aea408009f96584ae5 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 11:59:54 +0200 Subject: [PATCH 172/322] perf: crack project references in parallel (#5079) --- src/Fable.Compiler/MSBuildCrackerResolver.fs | 100 +++++++++++++------ src/Fable.Compiler/ProjectCracker.fs | 82 +++++++++++---- src/Fable.Compiler/ProjectCracker.fsi | 1 + 3 files changed, 132 insertions(+), 51 deletions(-) diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index 53c16f9dda..a658d1f9d7 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -2,6 +2,7 @@ namespace Fable.Compiler open System open System.Reflection +open System.Collections.Concurrent open System.IO open System.Diagnostics open System.Text.Json @@ -101,28 +102,21 @@ module private MSBuildCrackerResolver = let private dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) : Async = dotnet_msbuild_with_defines cwd fsproj args List.empty - let mkOptionsFromDesignTimeBuildAux (fsproj: FileInfo) (options: CrackerOptions) : Async = + let private queryTargetFramework (fsproj: FileInfo) (options: CrackerOptions) (configurationProperty: string) = async { - let configurationProperty = - if String.IsNullOrWhiteSpace options.Configuration then - "" - else - $"/p:Configuration=%s{options.Configuration}" - let! targetFrameworkJson = dotnet_msbuild options.RootDir fsproj.FullName $"%s{configurationProperty} --getProperty:TargetFrameworks --getProperty:TargetFramework" - let targetFramework = - let properties = - try - JsonDocument.Parse targetFrameworkJson - |> fun json -> json.RootElement.GetProperty "Properties" - with ex -> - failwith - $"""Failed to parse MSBuild output + let properties = + try + JsonDocument.Parse targetFrameworkJson + |> fun json -> json.RootElement.GetProperty "Properties" + with ex -> + failwith + $"""Failed to parse MSBuild output JSON: %s{targetFrameworkJson} @@ -130,14 +124,34 @@ Exception: %O{ex} """ - let tf, tfs = - properties.GetProperty("TargetFramework").GetString(), - properties.GetProperty("TargetFrameworks").GetString() + let tf, tfs = + properties.GetProperty("TargetFramework").GetString(), + properties.GetProperty("TargetFrameworks").GetString() - if not (String.IsNullOrWhiteSpace tf) then - tf + if not (String.IsNullOrWhiteSpace tf) then + return tf + else + return tfs.Split ';' |> Array.head + } + + let mkOptionsFromDesignTimeBuildAux + (referenceTargetFrameworks: ConcurrentDictionary) + (isMain: bool) + (fsproj: FileInfo) + (options: CrackerOptions) + : Async + = + async { + let configurationProperty = + if String.IsNullOrWhiteSpace options.Configuration then + "" else - tfs.Split ';' |> Array.head + $"/p:Configuration=%s{options.Configuration}" + + let! targetFramework = + match referenceTargetFrameworks.TryGetValue fsproj.FullName with + | true, targetFramework -> async.Return targetFramework + | _ -> queryTargetFramework fsproj options configurationProperty // When CoreCompile does not need a rebuild, MSBuild will skip that target and thus will not populate the FscCommandLineArgs items. @@ -152,13 +166,6 @@ Exception: "/p:DesignTimeBuild=True" "/p:SkipCompilerExecution=True" "/p:ProvideCommandLineArgs=True" - // See https://github.com/NuGet/Home/issues/13046 - "/p:RestoreUseStaticGraphEvaluation=False" - // Avoid restoring with an existing lock file - "/p:RestoreLockedMode=false" - "/p:RestorePackagesWithLockFile=false" - // We trick NuGet into believing there is no lock file create, if it does exist it will try and create it. - " /p:NuGetLockFilePath=Fable.lock" // Avoid skipping the CoreCompile target via this property. $"/p:NonExistentFile=\"%s{nonExistentFile}\"" ] @@ -167,14 +174,31 @@ Exception: let targets = "ResolveAssemblyReferencesDesignTime,ResolveProjectReferencesDesignTime,ResolvePackageDependenciesDesignTime,FindReferenceAssembliesForReferences,_GenerateCompileDependencyCache,_ComputeNonExistentFileProperty,BeforeBuild,BeforeCompile,CoreCompile" + // MSBuild ignores the -p properties during restore when -restoreProperty is given, so the restore covers every TargetFramework let restoreArg = - if options.NoRestore then + if options.NoRestore || not isMain then "" else - "/restore" + let restoreProperties = + [ + if not (String.IsNullOrWhiteSpace options.Configuration) then + $"Configuration=%s{options.Configuration}" + // See https://github.com/NuGet/Home/issues/13046 + "RestoreUseStaticGraphEvaluation=False" + // Avoid restoring with an existing lock file + "RestoreLockedMode=false" + "RestorePackagesWithLockFile=false" + // We trick NuGet into believing there is no lock file create, if it does exist it will try and create it. + "NuGetLockFilePath=Fable.lock" + for constant in options.FableOptions.Define do + $"%s{constant}=True" + ] + |> String.concat ";" + + $"/restore -restoreProperty:%s{restoreProperties}" let arguments = - $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getProperty:OutputType -warnAsMessage:NU1608" + $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType -warnAsMessage:NU1608" let! json = dotnet_msbuild_with_defines options.RootDir fsproj.FullName arguments options.FableOptions.Define @@ -217,6 +241,17 @@ Exception: ) |> Seq.toArray + match items.TryGetProperty "_MSBuildProjectReferenceExistent" with + | true, existentReferences -> + for reference in existentReferences.EnumerateArray() do + match reference.TryGetProperty "TargetFrameworks" with + | true, targetFrameworks when not (String.IsNullOrWhiteSpace(targetFrameworks.GetString())) -> + let referencePath = FileInfo(reference.GetProperty("FullPath").GetString()).FullName + + referenceTargetFrameworks[referencePath] <- targetFrameworks.GetString().Split(';').[0] + | _ -> () + | _ -> () + let outputType = properties.GetProperty("OutputType").GetString() return @@ -229,6 +264,7 @@ Exception: } type MSBuildCrackerResolver() = + let referenceTargetFrameworks = ConcurrentDictionary() interface ProjectCrackerResolver with member _.GetProjectOptionsFromProjectFile(isMain, options: CrackerOptions, projectFile) = @@ -239,7 +275,7 @@ type MSBuildCrackerResolver() = // Bad I know... let result = - MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux fsproj options + MSBuildCrackerResolver.mkOptionsFromDesignTimeBuildAux referenceTargetFrameworks isMain fsproj options |> Async.RunSynchronously result diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index bf2ee2705d..67d212cc50 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -6,6 +6,7 @@ open System open System.Xml.Linq open System.Text.RegularExpressions open System.Collections.Generic +open System.Threading.Tasks open FSharp.Compiler.CodeAnalysis open FSharp.Compiler.Text open Fable @@ -113,6 +114,7 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = getFableModulesFromProject (projDir, cliArgs.OutDir, cliArgs.NoCache, cliArgs.NoGitignore, evaluateOnly) let builtDlls = HashSet() + let pendingDllBuilds = Dictionary>() let cacheInfo = if cliArgs.NoCache then @@ -138,7 +140,10 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = member _.EvaluateOnly: bool = evaluateOnly member _.BuildDll(normalizedDllPath: string) = - if not (builtDlls.Contains(normalizedDllPath)) then + if + not (builtDlls.Contains(normalizedDllPath)) + && not (pendingDllBuilds.ContainsKey(normalizedDllPath)) + then let projDir = normalizedDllPath.Split('/') |> Array.rev @@ -151,11 +156,20 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = |> Array.rev |> String.concat "/" - let stdout = - Process.runSyncWithOutput projDir "dotnet" [ "build"; "-c"; cliArgs.Configuration ] + pendingDllBuilds[normalizedDllPath] <- + Task.Run(fun () -> + Process.runSyncWithOutput projDir "dotnet" [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] + ) - Log.always stdout + member _.WaitForBuiltDlls() = + for KeyValue(normalizedDllPath, build) in pendingDllBuilds |> Seq.toArray do + try + build.GetAwaiter().GetResult() |> Log.always + with e -> + let projName = IO.Path.GetFileNameWithoutExtension(normalizedDllPath) + Log.always ("Couldn't build " + projName + ": " + e.Message) + pendingDllBuilds.Remove(normalizedDllPath) |> ignore builtDlls.Add(normalizedDllPath) |> ignore member _.ResetFableModulesDir() = @@ -566,15 +580,12 @@ let crackMainProject (resolver: ProjectCrackerResolver) (opts: CrackerOptions) : /// For project references of main project, ignore dll and package references let crackReferenceProject - (resolver: ProjectCrackerResolver) (opts: CrackerOptions) dllRefs (projFile: string) + (projectOptionsResponse: ProjectOptionsResponse) : CrackedFsproj = - let projectOptionsResponse = - resolver.GetProjectOptionsFromProjectFile(false, opts, projFile) - let sourceFiles, otherOpts = Array.foldBack (extractUsefulOptionsAndSources false) projectOptionsResponse.ProjectOptions ([], []) @@ -600,27 +611,60 @@ let getCrackedProjectsFromMainFsproj : CrackedFsproj list * CrackedFsproj = let mainProj = crackMainProject resolver opts + let crackedRefs = Dictionary() + + let rec crackLevel (projFiles: string list) = + let pending = + projFiles |> List.distinct |> List.filter (crackedRefs.ContainsKey >> not) + + if not (List.isEmpty pending) then + let responses = + pending + |> List.chunkBySize Environment.ProcessorCount + |> List.collect (fun chunk -> + chunk + |> List.map (fun projFile -> + Task.Factory.StartNew( + (fun () -> resolver.GetProjectOptionsFromProjectFile(false, opts, projFile)), + TaskCreationOptions.LongRunning + ) + ) + |> List.map _.GetAwaiter().GetResult() + ) + + List.zip pending responses + |> List.collect (fun (projFile, response) -> + let crackedFsproj = + crackReferenceProject opts mainProj.DllReferences projFile response + + crackedRefs[projFile] <- crackedFsproj + crackedFsproj.ProjectReferences + ) + |> crackLevel + + crackLevel mainProj.ProjectReferences - let rec crackProjects (acc: CrackedFsproj list) (projFile: string) = - let crackedFsproj = - match acc |> List.tryFind (fun x -> x.ProjectFile = projFile) with - | None -> crackReferenceProject resolver opts mainProj.DllReferences projFile - | Some crackedFsproj -> crackedFsproj + let rec addProjects (acc: CrackedFsproj list) (projFile: string) = + let crackedFsproj = crackedRefs[projFile] // Add always a reference to the front to preserve compilation order // Duplicated items will be removed later - List.fold crackProjects (crackedFsproj :: acc) crackedFsproj.ProjectReferences + List.fold addProjects (crackedFsproj :: acc) crackedFsproj.ProjectReferences let refProjs = - List.fold crackProjects [] mainProj.ProjectReferences + List.fold addProjects [] mainProj.ProjectReferences |> List.distinctBy (fun x -> x.ProjectFile) refProjs, mainProj let getCrackedProjects (resolver: ProjectCrackerResolver) (opts: CrackerOptions) = - match (Path.GetExtension opts.ProjFile).ToLower() with - | ".fsx" -> [], getProjectOptionsFromScript opts - | ".fsproj" -> getCrackedProjectsFromMainFsproj resolver opts - | s -> failwith $"Unsupported project type: %s{s}" + let crackedProjects = + match (Path.GetExtension opts.ProjFile).ToLower() with + | ".fsx" -> [], getProjectOptionsFromScript opts + | ".fsproj" -> getCrackedProjectsFromMainFsproj resolver opts + | s -> failwith $"Unsupported project type: %s{s}" + + opts.WaitForBuiltDlls() + crackedProjects // It is common for editors with rich editing or 'intellisense' to also be watching the project // file for changes. In some cases that editor will lock the file which can cause fable to diff --git a/src/Fable.Compiler/ProjectCracker.fsi b/src/Fable.Compiler/ProjectCracker.fsi index b742f5b1ed..0ddd4bdd32 100644 --- a/src/Fable.Compiler/ProjectCracker.fsi +++ b/src/Fable.Compiler/ProjectCracker.fsi @@ -48,6 +48,7 @@ type CrackerOptions = member SourceMapsRoot: string option member EvaluateOnly: bool member BuildDll: normalizedDllPath: string -> unit + member WaitForBuiltDlls: unit -> unit type CrackerResponse = { From 594d17b4b551ae33b5f2df5e4049611b9dd24bc2 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 19:38:46 +0200 Subject: [PATCH 173/322] perf: update inline expressions incrementally in watch mode (#5080) --- src/Fable.Transforms/State.fs | 45 +++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 9106c58ae8..fc414d6894 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -179,6 +179,12 @@ type PrecompiledInfo = abstract TryGetRootModule: normalizedFullPath: string -> string option abstract TryGetInlineExpr: memberUniqueName: string -> InlineExpr option +let private removeAll keys map = + (map, keys) ||> Seq.fold (fun map key -> Map.remove key map) + +let private addAll entries map = + (map, entries) ||> Seq.fold (fun map (key, value) -> Map.add key value map) + type Project private ( @@ -186,12 +192,14 @@ type Project projectOptions: FSharpProjectOptions, implFiles: Map, assemblies: Assemblies, - ?precompiledInfo: PrecompiledInfo + ?precompiledInfo: PrecompiledInfo, + ?inlineExprs: Map ) = - let inlineExprsDic = - implFiles |> Map.values |> Seq.collect (fun f -> f.InlineExprs) |> dict + let inlineExprs = + inlineExprs + |> Option.defaultWith (fun () -> implFiles |> Map.values |> Seq.collect (fun f -> f.InlineExprs) |> Map) let precompiledInfo = precompiledInfo @@ -234,19 +242,36 @@ type Project Project(projectFile, projectOptions, implFilesMap, assemblies, ?precompiledInfo = precompiledInfo) member this.Update(files: FSharpImplementationFileContents list) = - let implFiles = - (this.ImplementationFiles, files) - ||> List.fold (fun implFiles file -> + let implFiles, inlineExprs = + ((this.ImplementationFiles, inlineExprs), files) + ||> List.fold (fun (implFiles, inlineExprs) file -> let key = Path.normalizePathAndEnsureFsExtension file.FileName let file = ImplFile.From(file) - Map.add key file implFiles + + let previousInlineExprs = + match Map.tryFind key implFiles with + | Some previous -> previous.InlineExprs + | None -> [] + + let inlineExprs = + inlineExprs + |> removeAll (List.map fst previousInlineExprs) + |> addAll file.InlineExprs + + Map.add key file implFiles, inlineExprs ) - Project(this.ProjectFile, this.ProjectOptions, implFiles, this.Assemblies, this.PrecompiledInfo) + Project( + this.ProjectFile, + this.ProjectOptions, + implFiles, + this.Assemblies, + this.PrecompiledInfo, + inlineExprs = inlineExprs + ) member _.TryGetInlineExpr(com: Compiler, memberUniqueName: string) = - inlineExprsDic - |> Dictionary.tryFind memberUniqueName + Map.tryFind memberUniqueName inlineExprs |> Option.map (fun e -> e.Calculate(com)) member _.GetFileInlineExprs(com: Compiler) : (string * InlineExpr) array = From 5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 15 Oct 2026 22:23:13 +0200 Subject: [PATCH 174/322] fix(js/ts/python): preserve cancelled cleanup and generic unit callbacks (#5062) Co-authored-by: Maxime Mangel --- .../Python/Fable2Python.Transforms.fs | 81 +++-- .../Python/Fable2Python.Types.fs | 1 + .../Python/Fable2Python.Util.fs | 45 +++ src/fable-library-py/fable_library/async_.py | 68 +++- .../fable_library/async_builder.py | 56 +++- .../fable_library/mailbox_processor.py | 25 +- src/fable-library-py/fable_library/util.py | 5 + src/fable-library-ts/Async.ts | 21 +- src/fable-library-ts/AsyncBuilder.ts | 17 +- src/fable-library-ts/MailboxProcessor.ts | 2 + tests/Js/Main/AsyncTests.fs | 161 +++++++++ tests/Python/TestAsync.fs | 231 +++++++++++++ tests/Python/TestMailboxProcessor.fs | 14 + tests/Python/test_async_runtime.py | 307 ++++++++++++++++++ 14 files changed, 963 insertions(+), 71 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index d0bb7480da..c445abfde4 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -757,11 +757,24 @@ let transformObjectExpr Expression.call (Expression.name name), thisCaptureStmts @ [ stmt ] @ stmts -let transformCallArgs +let transformArgWithAdapters (com: IPythonCompiler) ctx (sigType: Fable.Type option) (argExpr: Fable.Expr) = + let expr, stmts = com.TransformAsExpr(ctx, argExpr) + + if needsOptionWidenForArg sigType argExpr then + let widen = com.TransformImport(ctx, "widen", getLibPath com "option") + Expression.call (widen, [ expr ]), stmts + elif needsUnitCallableEraseForArg sigType argExpr then + let erase = com.TransformImport(ctx, "erase_unit_callable", getLibPath com "util") + Expression.call (erase, [ expr ]), stmts + else + expr, stmts + +let transformCallArgsWith (com: IPythonCompiler) ctx (callInfo: Fable.CallInfo) (isBaseConstructorCall: bool) + (fallbackSigTypes: Fable.Type list) : Expression list * Keyword list * Statement list = @@ -854,15 +867,15 @@ let transformCallArgs let hasSpread = paramsInfo |> Option.map (fun i -> i.HasSpread) |> Option.defaultValue false - // Helper to transform an arg and wrap with widen() if needed - let transformArgWithWiden (sigType: Fable.Type option) (argExpr: Fable.Expr) = - let expr, stmts = com.TransformAsExpr(ctx, argExpr) + let transformArgs args = + args + |> List.mapi (fun i e -> + let sigType = + List.tryItem i callInfo.SignatureArgTypes + |> Option.orElse (List.tryItem i fallbackSigTypes) - if needsOptionWidenForArg sigType argExpr then - let widen = com.TransformImport(ctx, "widen", getLibPath com "option") - Expression.call (widen, [ expr ]), stmts - else - expr, stmts + transformArgWithAdapters com ctx sigType e + ) let args, stmts' = match args with @@ -871,31 +884,30 @@ let transformCallArgs match List.rev args with | [] -> [], [] | Replacements.Util.ArrayOrListLiteral(spreadArgs, _) :: rest -> - let rest = List.rev rest |> List.map (fun e -> com.TransformAsExpr(ctx, e)) + let rest = List.rev rest |> transformArgs rest @ List.map (fun e -> com.TransformAsExpr(ctx, e)) spreadArgs |> Helpers.unzipArgs | last :: rest -> - let rest, stmts = - List.rev rest - |> List.map (fun e -> com.TransformAsExpr(ctx, e)) - |> Helpers.unzipArgs + let rest, stmts = List.rev rest |> transformArgs |> Helpers.unzipArgs let expr, stmts' = com.TransformAsExpr(ctx, last) rest @ [ Expression.starred expr ], stmts @ stmts' - | args -> - // Transform args with widen() where needed based on signature types - args - |> List.mapi (fun i e -> - let sigType = List.tryItem i callInfo.SignatureArgTypes - transformArgWithWiden sigType e - ) - |> Helpers.unzipArgs + | args -> args |> transformArgs |> Helpers.unzipArgs match objArg with | None -> args, [], stmts @ stmts' | Some objArg -> args, objArg, stmts @ stmts' +let transformCallArgs + (com: IPythonCompiler) + ctx + (callInfo: Fable.CallInfo) + (isBaseConstructorCall: bool) + : Expression list * Keyword list * Statement list + = + transformCallArgsWith com ctx callInfo isBaseConstructorCall [] + let resolveExpr (ctx: Context) _t strategy pyExpr : Statement list = // printfn "resolveExpr: %A" (pyExpr, strategy) match strategy with @@ -1281,7 +1293,8 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI let callee', stmts = com.TransformAsExpr(ctx, callee) - let args, kw, stmts' = transformCallArgs com ctx callInfo false + let args, kw, stmts' = + transformCallArgsWith com ctx callInfo false (calleeSignatureArgTypes ctx callee callInfo) match callee, callInfo.ThisArg with | Fable.Get(expr, Fable.FieldGet { Name = "Dispose" }, _, _), _ -> @@ -1304,9 +1317,17 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI Expression.call (callee', args, kw, ?loc = range), stmts @ stmts' | _, None -> callFunction range callee' args kw, stmts @ stmts' -let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) args = - ((applied, stmts), args) - ||> List.fold (fun (applied, stmts) arg -> +let transformCurriedApply (com: IPythonCompiler) ctx range (callee: Fable.Expr) args = + let rec expectedArgTypes typ args = + match typ, args with + | Fable.LambdaType(argType, returnType), _ :: rest -> Some argType :: expectedArgTypes returnType rest + | _, _ :: rest -> None :: expectedArgTypes typ rest + | _, [] -> [] + + let applied, stmts = com.TransformAsExpr(ctx, callee) + + ((applied, stmts), List.zip (expectedArgTypes callee.Type args) args) + ||> List.fold (fun (applied, stmts) (sigType, arg) -> let args, argStmts = match arg with // TODO: If arg type is unit but it's an expression with potential @@ -1316,7 +1337,7 @@ let transformCurriedApply com ctx range (TransformExpr com ctx (applied, stmts)) | Fable.Value(Fable.UnitConstant, _) -> [], [] | Fable.IdentExpr ident when ident.Type = Fable.Unit -> [], [] | arg -> - let argExpr, transformStmts = com.TransformAsExpr(ctx, arg) + let argExpr, transformStmts = transformArgWithAdapters com ctx sigType arg // When a Call or CurriedApply result is passed as an argument, // check if we need to erase Option[T] to T | None let argExpr = @@ -2829,6 +2850,8 @@ let rec transformAsExpr (com: IPythonCompiler) ctx (expr: Fable.Expr) : Expressi iife com ctx expr | Fable.LetRec(bindings, body) -> + let ctx = withDeclaredFunctionTypes ctx bindings + if ctx.HoistVars(List.map fst bindings) then Expression.withStmts { let values, stmts = @@ -3030,10 +3053,13 @@ let rec transformAsStatements (com: IPythonCompiler) ctx returnStrategy (expr: F | Fable.Let(ident, value, body) -> let binding = transformBindingAsStatements com ctx ident value + let ctx = withDeclaredFunctionTypes ctx [ ident, value ] List.append binding (transformAsStatements com ctx returnStrategy body) | Fable.LetRec(bindings, body) -> + let ctx = withDeclaredFunctionTypes ctx bindings + let bindings = bindings |> Seq.collect (fun (i, v) -> transformBindingAsStatements com ctx i v) @@ -5466,6 +5492,7 @@ let transformFile (com: IPythonCompiler) (file: Fable.File) = ScopedTypeParams = Set.empty TypeParamsScope = 0 NarrowedTypes = Map.empty + DeclaredFunctionTypes = Map.empty EnclosingUnionBaseClass = None InEmitMacroArgument = false } diff --git a/src/Fable.Transforms/Python/Fable2Python.Types.fs b/src/Fable.Transforms/Python/Fable2Python.Types.fs index 2e9536484a..93dad4224e 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Types.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Types.fs @@ -124,6 +124,7 @@ type Context = ScopedTypeParams: Set TypeParamsScope: int NarrowedTypes: Map + DeclaredFunctionTypes: Map /// When inside a union base class definition, this holds the entity name. /// Used to determine whether to use base class name or type alias for annotations. EnclosingUnionBaseClass: string option diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index e449751b98..40fe337adb 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -319,6 +319,51 @@ module Util = | _ -> false | _ -> false + let needsUnitCallableEraseForArg (expectedType: Fable.Type option) (argExpr: Fable.Expr) = + let rec mismatches expected actual = + match FableTransforms.uncurryType expected, FableTransforms.uncurryType actual with + | Fable.DelegateType([ Fable.GenericParam _ ], _), Fable.DelegateType([ Fable.Unit ], _) -> true + | Fable.DelegateType(expectedArgs, _), Fable.DelegateType(actualArgs, _) + | Fable.Tuple(expectedArgs, _), Fable.Tuple(actualArgs, _) -> + List.sameLength expectedArgs actualArgs + && List.exists2 mismatches expectedArgs actualArgs + | _ -> false + + expectedType + |> Option.exists (fun expected -> + match FableTransforms.uncurryType expected, FableTransforms.uncurryType argExpr.Type with + | Fable.DelegateType(expectedArgs, _), Fable.DelegateType(actualArgs, _) -> + List.sameLength expectedArgs actualArgs + && List.exists2 mismatches expectedArgs actualArgs + | _ -> false + ) + + // Identifiers at call sites carry the instantiated type, not the declared generic one + let withDeclaredFunctionTypes (ctx: Context) (bindings: (Fable.Ident * Fable.Expr) list) = + let declared = + (ctx.DeclaredFunctionTypes, bindings) + ||> List.fold (fun declared (ident, value) -> + match value with + | Fable.Lambda _ + | Fable.Delegate _ -> Map.add ident.Name ident.Type declared + | _ -> Map.remove ident.Name declared + ) + + { ctx with DeclaredFunctionTypes = declared } + + // uncurryApplications emits an empty SignatureArgTypes for local function calls + let calleeSignatureArgTypes (ctx: Context) (callee: Fable.Expr) (callInfo: Fable.CallInfo) = + let calleeType = + match callee with + | Fable.IdentExpr ident -> + Map.tryFind ident.Name ctx.DeclaredFunctionTypes + |> Option.defaultValue callee.Type + | _ -> callee.Type + + match callInfo.SignatureArgTypes, FableTransforms.uncurryType calleeType with + | [], Fable.DelegateType(argTypes, _) when List.sameLength argTypes callInfo.Args -> argTypes + | _ -> [] + /// Wraps None values in cast(type, None) for type safety. /// Skips if type annotation is also None (unit type). let wrapNoneInCast (com: IPythonCompiler) ctx (value: Expression) (typeAnnotation: Expression) : Expression = diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 0e06c6fffd..4f82c01e59 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -7,6 +7,7 @@ from threading import RLock, Timer from typing import ( Any, + cast, ) from .array_ import Array @@ -33,7 +34,7 @@ ) from .event import IEvent_2 from .protocols import IEnumerable_1 -from .system import TimeoutException__ctor +from .system import InvalidOperationException__ctor_Z721C83C5, TimeoutException__ctor from .task import TaskCompletionSource from .time_span import TimeSpan, to_milliseconds from .types import UNIT, Unit @@ -228,16 +229,42 @@ def on_error(err: Exception): return protected_cont(cont) -def from_continuations[T]( - f: Callable[ - [Continuations[T]], - None, - ], -) -> Callable[[IAsyncContext[T]], None]: +def from_continuations[T](f: Callable[[Continuations[T]], None]) -> Async[T]: def cont(ctx: IAsyncContext[T]) -> None: - f((ctx.on_success, ctx.on_error, ctx.on_cancel)) + settled = False + gate = RLock() - return protected_cont(cont) + def claim() -> None: + nonlocal settled + with gate: + if settled: + raise InvalidOperationException__ctor_Z721C83C5("A continuation was invoked more than once") + settled = True + + # Compiled F# calls the success continuation with no argument when T is unit + def success(value: T = cast(T, None)) -> None: + claim() + ctx.on_success(value) + + def error(value: Exception) -> None: + claim() + ctx.on_error(value) + + def cancelled(value: OperationCanceledError) -> None: + claim() + ctx.on_cancel(value) + + def on_exception(value: Exception) -> None: + with gate: + completed = settled + if completed: + raise value + error(value) + + guarded = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, success, on_exception, cancelled) + protected_cont(lambda _: f((success, error, cancelled)))(guarded) + + return cont def from_continuations_unit( @@ -246,13 +273,11 @@ def from_continuations_unit( None, ], ) -> Async[None]: - def cont(ctx: IAsyncContext[None]) -> None: - def success(value: None = None) -> None: - ctx.on_success(value) + def callback(conts: Continuations[None]) -> None: + success, error, cancelled = conts + f((lambda: success(None), error, cancelled)) - f((success, ctx.on_error, ctx.on_cancel)) - - return protected_cont(cont) + return from_continuations(callback) def await_task[T](task: Awaitable[T]) -> Async[T]: @@ -327,12 +352,19 @@ def start_with_continuations[T]( token is used.""" trampoline = Trampoline() + def done[U](cont: Callable[[U], None]) -> Callable[[U], None]: + def terminal(value: U) -> None: + trampoline.completed = True + cont(value) + + return terminal + ctx = IAsyncContext.create( trampoline, cancellation_token or default_cancellation_token, - continuation or empty_continuation, - exception_continuation or empty_continuation, - cancellation_continuation or empty_continuation, + done(continuation or empty_continuation), + done(exception_continuation or empty_continuation), + done(cancellation_continuation or empty_continuation), ) def runner() -> None: diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index c3eef16f2d..e3f43ead41 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -233,7 +233,7 @@ class ScheduledItem: class Trampoline: - __slots__ = "call_count", "lock", "running" + __slots__ = "call_count", "completed", "lock", "running" MaxTrampolineCallCount = 75 # Max recursion depth: 1000 @@ -241,6 +241,8 @@ def __init__(self) -> None: self.call_count: int = 0 self.lock = Lock() self.running = False + # After a terminal continuation runs, exceptions reaching protected_cont come from continuation code + self.completed = False def increment_and_check(self): with self.lock: @@ -256,13 +258,17 @@ def run_later( return loop.call_later(due_time, action) def run(self, action: Callable[[], None]): - loop = asyncio.get_running_loop() + if not self.increment_and_check(): + action() + return - if self.increment_and_check(): - self.call_count = 0 - loop.call_soon(action) - else: + self.call_count = 0 + try: + loop = asyncio.get_running_loop() + except RuntimeError: action() + return + loop.call_soon(action) def protected_cont[T](f: Async[T]) -> Async[T]: @@ -275,7 +281,8 @@ def fn(): try: return f(ctx) except Exception as err: - # print("Exception: ", err) + if ctx.trampoline.completed: + raise ctx.on_error(err) ctx.trampoline.run(fn) @@ -290,10 +297,11 @@ def protected_bind[T, U]( def cont(ctx: IAsyncContext[U]) -> None: def on_success(x: T) -> None: try: - binder(x)(ctx) + bound = binder(x) except Exception as err: - # print("Exception: ", err) ctx.on_error(err) + return + bound(ctx) ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, on_success, ctx.on_error, ctx.on_cancel) return computation(ctx_) @@ -350,7 +358,21 @@ def ReturnFrom[T](self, computation: Async[T]) -> Async[T]: def TryFinally[T](self, computation: Async[T], compensation: Callable[[], None]) -> Async[T]: def cont(ctx: IAsyncContext[T]) -> None: + settled = False + gate = Lock() + + def claim() -> bool: + nonlocal settled + # on_cancel may run on another thread while on_success runs on the loop + with gate: + if settled: + return False + settled = True + return True + def on_success(x: T) -> None: + if not claim(): + return try: compensation() except Exception as error: @@ -359,6 +381,8 @@ def on_success(x: T) -> None: ctx.on_success(x) def on_error(x: Exception) -> None: + if not claim(): + raise x try: compensation() except Exception as error: @@ -367,6 +391,8 @@ def on_error(x: Exception) -> None: ctx.on_error(x) def on_cancel(x: OperationCanceledError) -> None: + if not claim(): + return # decision: ignores compensation failures during cancellation, matching .NET Async.TryFinally try: compensation() @@ -375,17 +401,20 @@ def on_cancel(x: OperationCanceledError) -> None: ctx.on_cancel(x) ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, on_success, on_error, on_cancel) - computation(ctx_) + # The cancellation check in protected_cont must see the cleanup continuations + protected_cont(computation)(ctx_) - return protected_cont(cont) + return cont def TryWith[T](self, computation: Async[T], catch_handler: Callable[[Exception], Async[T]]) -> Async[T]: def fn(ctx: IAsyncContext[T]): def on_error(err: Exception) -> None: try: - catch_handler(err)(ctx) + handled = catch_handler(err) except Exception as ex2: ctx.on_error(ex2) + return + handled(ctx) ctx_ = IAsyncContext.create( on_success=ctx.on_success, @@ -403,7 +432,8 @@ def Using[D: IDisposable, U](self, resource: D, binder: Callable[[D], Async[U]]) def compensation() -> None: return resource.Dispose() - return self.TryFinally(binder(resource), compensation) + # Without Delay the binder runs before TryFinally installs Dispose + return self.TryFinally(self.Delay(lambda: binder(resource)), compensation) @overload def While(self, guard: Callable[[], bool], computation: Async[Literal[None]]) -> Async[None]: ... diff --git a/src/fable-library-py/fable_library/mailbox_processor.py b/src/fable-library-py/fable_library/mailbox_processor.py index cbdbe5f020..ef8bc9e734 100644 --- a/src/fable-library-py/fable_library/mailbox_processor.py +++ b/src/fable-library-py/fable_library/mailbox_processor.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Callable from queue import SimpleQueue -from threading import RLock +from threading import Lock, RLock from typing import Any from .async_ import call_on_owner_loop, from_continuations, start_immediate @@ -73,18 +73,30 @@ def post_and_async_reply[Reply](self, build_message: Callable[[AsyncReplyChannel # Track whether `reply` was actually called, since the reply value itself # may legitimately be `None` (e.g. `AsyncReplyChannel[None]` for unit). replied = False + delivered = False + gate = Lock() continuation: Continuations[Any] | None = ( None # This is the continuation for the `done` callback of the awaiting poster. ) def check_completion() -> None: - if replied and continuation is not None: - continuation[0](result) + nonlocal delivered + # reply may run on another thread before the poster's continuation is installed + with gate: + if delivered or not replied or continuation is None: + return + delivered = True + deliver = continuation[0] + deliver(result) def reply_callback(res: Reply): nonlocal result, replied - result = res - replied = True + # .NET AsyncReplyChannel.Reply ignores a second reply + with gate: + if replied: + return + result = res + replied = True check_completion() reply_channel: AsyncReplyChannel[Reply] = AsyncReplyChannel(reply_callback) @@ -93,7 +105,8 @@ def reply_callback(res: Reply): def callback(conts: Continuations[Any]) -> None: nonlocal continuation - continuation = conts + with gate: + continuation = conts check_completion() return from_continuations(callback) diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index 1d3f5648ed..a808660b3b 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -56,6 +56,10 @@ from .types import UNIT, Unit +def erase_unit_callable[**P, R](callback: Callable[P, R], /) -> Callable[..., R]: + return callback + + class nullable: """Returns a tuple of None values with the specified types. @@ -963,6 +967,7 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "Unit", "array_hash", "copy_to_array", + "erase_unit_callable", "escape_data_string", "escape_uri_string", "create_random", diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index 23ab998b7e..09eaabee44 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -4,7 +4,7 @@ import { Async, IAsyncContext, CancellationToken } from "./AsyncBuilder.ts"; import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts"; import type { IEvent$2 } from "./Event.ts"; import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts"; -import { TimeoutException_$ctor } from "./System.ts"; +import { InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; import { Exception } from "./Util.ts"; function emptyContinuation(_x: T) { @@ -128,8 +128,23 @@ export function catchAsync(work: Async) { } export function fromContinuations(f: (conts: Continuations) => void) { - return protectedCont((ctx: IAsyncContext) => - f([ctx.onSuccess, ctx.onError, ctx.onCancel])); + return (ctx: IAsyncContext) => { + let settled = false; + const claim = () => { + if (settled) { throw InvalidOperationException_$ctor_Z721C83C5("A continuation was invoked more than once"); } + settled = true; + }; + const success = (x: T) => { claim(); ctx.onSuccess(x); }; + const error = (x: Exception) => { claim(); ctx.onError(x); }; + const cancelled = (x: OperationCanceledException) => { claim(); ctx.onCancel(x); }; + protectedCont(() => f([success, error, cancelled]))({ + onSuccess: success, + onError: (x: Exception) => { if (settled) { throw x; } error(x); }, + onCancel: cancelled, + cancelToken: ctx.cancelToken, + trampoline: ctx.trampoline, + }); + }; } export function ignore(computation: Async) { diff --git a/src/fable-library-ts/AsyncBuilder.ts b/src/fable-library-ts/AsyncBuilder.ts index 2a89be285e..0a89c59e18 100644 --- a/src/fable-library-ts/AsyncBuilder.ts +++ b/src/fable-library-ts/AsyncBuilder.ts @@ -200,9 +200,13 @@ export class AsyncBuilder { } public TryFinally(computation: Async, compensation: () => void) { - return protectedCont((ctx: IAsyncContext) => { - computation({ + // The cancellation check in protectedCont must see the cleanup continuations + return (ctx: IAsyncContext) => { + let settled = false; + protectedCont(computation)({ onSuccess: (x: T) => { + if (settled) { return; } + settled = true; try { compensation(); } catch (error) { ctx.onError(ensureErrorOrException(error)); return; @@ -210,6 +214,8 @@ export class AsyncBuilder { ctx.onSuccess(x); }, onError: (x: any) => { + if (settled) { throw x; } + settled = true; try { compensation(); } catch (error) { ctx.onError(ensureErrorOrException(error)); return; @@ -217,6 +223,8 @@ export class AsyncBuilder { ctx.onError(x); }, onCancel: (x: any) => { + if (settled) { return; } + settled = true; // decision: cancellation remains terminal when compensation throws, matching .NET Async.TryFinally try { compensation(); } catch { /* Cancellation takes precedence. */ } ctx.onCancel(x); @@ -224,7 +232,7 @@ export class AsyncBuilder { cancelToken: ctx.cancelToken, trampoline: ctx.trampoline, }); - }); + }; } public TryWith(computation: Async, catchHandler: (e: any) => Async) { @@ -250,7 +258,8 @@ export class AsyncBuilder { } public Using(resource: T, binder: (x: T) => Async) { - return this.TryFinally(binder(resource), () => resource.Dispose()); + // Without Delay the binder runs before TryFinally installs Dispose + return this.TryFinally(this.Delay(() => binder(resource)), () => resource.Dispose()); } public While(guard: () => boolean, computation: Async): Async { diff --git a/src/fable-library-ts/MailboxProcessor.ts b/src/fable-library-ts/MailboxProcessor.ts index b1771bf70a..c4d08b3b24 100644 --- a/src/fable-library-ts/MailboxProcessor.ts +++ b/src/fable-library-ts/MailboxProcessor.ts @@ -123,6 +123,8 @@ export function postAndAsyncReply( } const reply = { reply: (res: Reply) => { + // .NET AsyncReplyChannel.Reply ignores a second reply + if (replied) { return; } result = res; replied = true; checkCompletion(); diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 362207952e..fc17756520 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -4,6 +4,113 @@ open System open Util open Util.Testing +module private FinalizerRegressions = + open System + open System.Threading + + let private check expected actual = + if expected <> actual then failwithf "Expected %A, got %A" expected actual + + let private observe (token: CancellationToken) work = + let terminal = ResizeArray() + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun e -> terminal.Add e.Message), (fun _ -> terminal.Add "cancel"), token) + terminal + + let preCancelled () = + for throws in [false; true] do + use source = new CancellationTokenSource() + source.Cancel() + let mutable cleanups = 0 + let mutable entered = false + let work = async.TryFinally(async { entered <- true }, fun () -> + cleanups <- cleanups + 1 + if throws then failwith "cleanup") + let terminal = observe source.Token work + check false entered + check 1 cleanups + check [|"cancel"|] (terminal.ToArray()) + + let nestedPreCancelled () = + use source = new CancellationTokenSource() + source.Cancel() + let cleanups = ResizeArray() + let inner = async.TryFinally(async.Return 42, fun () -> cleanups.Add "inner") + let outer = async.TryFinally(inner, fun () -> cleanups.Add "outer") + let terminal = observe source.Token outer + check [|"outer"|] (cleanups.ToArray()) + check [|"cancel"|] (terminal.ToArray()) + + let cancelledAcquisition () = + for acquisition in [0; 1; 2] do + use source = new CancellationTokenSource() + let mutable disposals = 0 + let mutable entered = false + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let acquire = Async.FromContinuations(fun (ok, _, _) -> + source.Cancel() + ok resource) + let work = + if acquisition = 1 then async { + use! owned = acquire + entered <- true + } + elif acquisition = 0 then async { + let! acquired = acquire + use owned = acquired + entered <- true + } + else async { + use owned = + source.Cancel() + resource + entered <- true + } + let terminal = observe source.Token work + check false entered + check 1 disposals + check [|"cancel"|] (terminal.ToArray()) + + let normalAndFailure () = + for fails in [false; true] do + let mutable disposals = 0 + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let work = async { + use owned = resource + if fails then failwith "body" + return 42 + } + let terminal = observe CancellationToken.None work + check 1 disposals + check [|if fails then "body" else "success"|] (terminal.ToArray()) + + let trampoline () = async { + let mutable cleanups = 0 + for _ in 1 .. 10000 do + do! async.TryFinally(async { return () }, fun () -> cleanups <- cleanups + 1) + check 10000 cleanups + } + + let downstreamExceptions () = + for channel in ["success"; "error"; "cancel"] do + let mutable cleanups = 0 + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (ok, error, cancelled) -> + match channel with + | "success" -> ok () + | "error" -> error (Exception "body") + | _ -> cancelled (OperationCanceledException())) + let terminal name = calls.Add name; failwith "downstream" + let caught = + try + Async.StartWithContinuations( + async.TryFinally(body, fun () -> cleanups <- cleanups + 1), + (fun () -> terminal "success"), (fun _ -> terminal "error"), (fun _ -> terminal "cancel")) + "no exception" + with e -> e.Message + check "downstream" caught + check 1 cleanups + check [|channel|] (calls.ToArray()) + #if FABLE_COMPILER open Fable.Core @@ -57,8 +164,31 @@ let asyncMap f a = async { return f a } +let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (OperationCanceledException -> unit) -> unit) : Async<'T> = + Async.FromContinuations(fun (ok, error, cancelled) -> callback ok error cancelled) + let tests = testList "Async" [ + testCase "TryFinally installs cleanup before pre-cancellation" <| FinalizerRegressions.preCancelled + testCase "Pre-cancelled nested TryFinally only runs outer cleanup" <| FinalizerRegressions.nestedPreCancelled + testCase "Cancelled acquisition disposes use and use bang resources" <| FinalizerRegressions.cancelledAcquisition + testCase "Resource cleanup runs once on success and failure" <| FinalizerRegressions.normalAndFailure + testCase "TryFinally propagates downstream continuation errors once" <| FinalizerRegressions.downstreamExceptions + testCaseAsync "TryFinally preserves bounded trampoline execution" <| FinalizerRegressions.trampoline + + testCase "Generic FromContinuations accepts erased unit callbacks" <| fun () -> + let calls = ResizeArray() + let start work = Async.StartWithContinuations(work, (fun _ -> calls.Add "success"), (fun e -> calls.Add e.Message), (fun e -> calls.Add e.Message)) + let unitWork: Async = fromGenericCallback (fun ok _ _ -> ok ()) + start unitWork + let valueWork = fromGenericCallback (fun ok _ _ -> ok 42) + Async.StartWithContinuations(valueWork, (fun value -> equal 42 value; calls.Add "value"), raise, raise) + let errorWork: Async = fromGenericCallback (fun _ error _ -> error (exn "error")) + start errorWork + let cancelWork: Async = fromGenericCallback (fun _ _ cancelled -> cancelled (OperationCanceledException("cancel"))) + start cancelWork + equal [|"success"; "value"; "error"; "cancel"|] (calls.ToArray()) + testCase "FromContinuations unit and generic values" <| fun () -> let fromValue value = Async.FromContinuations(fun (ok, _, _) -> ok value) let mutable completed = 0 @@ -68,6 +198,24 @@ let tests = Async.StartWithContinuations(fromValue 42, (fun value -> equal 42 value; completed <- completed + 1), raise, raise) equal 3 completed + testCase "FromContinuations rejects a second completion" <| fun () -> + let unitCalls = ResizeArray() + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok (); ok ()) + throwsAnyError (fun () -> + Async.StartWithContinuations(unitWork, (fun () -> unitCalls.Add "success"), (fun _ -> unitCalls.Add "error"), (fun _ -> unitCalls.Add "cancel"))) + equal true (unitCalls.Count <= 1) + equal false (unitCalls.Contains "error") + equal false (unitCalls.Contains "cancel") + + let valueCalls = ResizeArray() + let valueWork = Async.FromContinuations(fun (ok, _, _) -> ok 42; ok 43) + throwsAnyError (fun () -> + Async.StartWithContinuations(valueWork, (fun v -> valueCalls.Add (string v)), (fun _ -> valueCalls.Add "error"), (fun _ -> valueCalls.Add "cancel"))) + equal true (valueCalls.Count <= 1) + equal false (valueCalls.Contains "43") + equal false (valueCalls.Contains "error") + equal false (valueCalls.Contains "cancel") + testCase "Generic unit continuations work through recursive bindings and aliases" <| fun () -> let mutable recurse = true let rec fromValue value = @@ -702,6 +850,19 @@ let tests = equal true ran } + testCaseAsync "MailboxProcessor second Reply is ignored" <| fun () -> + async { + let log = ResizeArray() + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 1 + channel.Reply 2 + log.Add "body done" }) + let! reply = agent.PostAndAsyncReply(id) + equal 1 reply + equal [|"body done"|] (log.ToArray()) + } + testCase "Async try .. with returns correctly from 'with' branch" <| fun () -> let work = async { try diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index 713af3d5c6..c41e74086c 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -1,8 +1,116 @@ module Fable.Tests.Async open System +open Util open Util.Testing +module private FinalizerRegressions = + open System + open System.Threading + + let private check expected actual = + if expected <> actual then failwithf "Expected %A, got %A" expected actual + + let private observe (token: CancellationToken) work = + let terminal = ResizeArray() + Async.StartWithContinuations(work, (fun _ -> terminal.Add "success"), (fun e -> terminal.Add e.Message), (fun _ -> terminal.Add "cancel"), token) + terminal + + let preCancelled () = + for throws in [false; true] do + use source = new CancellationTokenSource() + source.Cancel() + let mutable cleanups = 0 + let mutable entered = false + let work = async.TryFinally(async { entered <- true }, fun () -> + cleanups <- cleanups + 1 + if throws then failwith "cleanup") + let terminal = observe source.Token work + check false entered + check 1 cleanups + check [|"cancel"|] (terminal.ToArray()) + + let nestedPreCancelled () = + use source = new CancellationTokenSource() + source.Cancel() + let cleanups = ResizeArray() + let inner = async.TryFinally(async.Return 42, fun () -> cleanups.Add "inner") + let outer = async.TryFinally(inner, fun () -> cleanups.Add "outer") + let terminal = observe source.Token outer + check [|"outer"|] (cleanups.ToArray()) + check [|"cancel"|] (terminal.ToArray()) + + let cancelledAcquisition () = + for acquisition in [0; 1; 2] do + use source = new CancellationTokenSource() + let mutable disposals = 0 + let mutable entered = false + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let acquire = Async.FromContinuations(fun (ok, _, _) -> + source.Cancel() + ok resource) + let work = + if acquisition = 1 then async { + use! owned = acquire + entered <- true + } + elif acquisition = 0 then async { + let! acquired = acquire + use owned = acquired + entered <- true + } + else async { + use owned = + source.Cancel() + resource + entered <- true + } + let terminal = observe source.Token work + check false entered + check 1 disposals + check [|"cancel"|] (terminal.ToArray()) + + let normalAndFailure () = + for fails in [false; true] do + let mutable disposals = 0 + let resource = { new IDisposable with member _.Dispose() = disposals <- disposals + 1 } + let work = async { + use owned = resource + if fails then failwith "body" + return 42 + } + let terminal = observe CancellationToken.None work + check 1 disposals + check [|if fails then "body" else "success"|] (terminal.ToArray()) + + let trampoline () = async { + let mutable cleanups = 0 + for _ in 1 .. 10000 do + do! async.TryFinally(async { return () }, fun () -> cleanups <- cleanups + 1) + check 10000 cleanups + } + + let downstreamExceptions () = + for channel in ["success"; "error"; "cancel"] do + let mutable cleanups = 0 + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (ok, error, cancelled) -> + match channel with + | "success" -> ok () + | "error" -> error (Exception "body") + | _ -> cancelled (OperationCanceledException())) + let terminal name = calls.Add name; failwith "downstream" + let caught = + try + Async.StartWithContinuations( + async.TryFinally(body, fun () -> cleanups <- cleanups + 1), + (fun () -> terminal "success"), (fun _ -> terminal "error"), (fun _ -> terminal "cancel")) + "no exception" + with e -> e.Message + check "downstream" caught + check 1 cleanups + check [|channel|] (calls.ToArray()) + #if FABLE_COMPILER module private Runtime = open Fable.Core.PyInterop @@ -957,3 +1065,126 @@ let ``test cancelling the mailbox token cancels a body waiting in Receive`` () = |> ignore) equal "cancelled" outcome } |> Async.RunSynchronously + +[] +let ``test TryFinally installs cleanup before pre-cancellation`` () = + FinalizerRegressions.preCancelled () + +[] +let ``test pre-cancelled nested TryFinally only runs outer cleanup`` () = + FinalizerRegressions.nestedPreCancelled () + +[] +let ``test cancelled acquisition disposes use and use bang resources`` () = + FinalizerRegressions.cancelledAcquisition () + +[] +let ``test resource cleanup runs once on success and failure`` () = + FinalizerRegressions.normalAndFailure () + +[] +let ``test TryFinally preserves bounded trampoline execution`` () = + FinalizerRegressions.trampoline () |> Async.RunSynchronously + +[] +let ``test TryFinally propagates downstream continuation errors once`` () = + FinalizerRegressions.downstreamExceptions () + +let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (OperationCanceledException -> unit) -> unit) : Async<'T> = + Async.FromContinuations(fun (ok, error, cancelled) -> callback ok error cancelled) + +[] +let ``test generic FromContinuations accepts erased unit callbacks`` () = + let calls = ResizeArray() + let start work = Async.StartWithContinuations(work, (fun _ -> calls.Add "success"), (fun e -> calls.Add e.Message), (fun e -> calls.Add e.Message)) + let unitWork: Async = fromGenericCallback (fun ok _ _ -> ok ()) + start unitWork + let valueWork = fromGenericCallback (fun ok _ _ -> ok 42) + Async.StartWithContinuations(valueWork, (fun value -> equal 42 value; calls.Add "value"), raise, raise) + let errorWork: Async = fromGenericCallback (fun _ error _ -> error (exn "error")) + start errorWork + let cancelWork: Async = fromGenericCallback (fun _ _ cancelled -> cancelled (OperationCanceledException("cancel"))) + start cancelWork + equal [|"success"; "value"; "error"; "cancel"|] (calls.ToArray()) + +let private invokeGenericCallback (callback: ('T -> unit) -> 'Result) (value: 'T) : 'Result = + callback (fun received -> equal value received) + +[] +let ``test generic unit callback adaptation preserves return types`` () = + equal 42 (invokeGenericCallback (fun ok -> ok (); 42) ()) + equal "unit" (invokeGenericCallback (fun ok -> ok (); "unit") ()) + equal "value" (invokeGenericCallback (fun ok -> ok 7; "value") 7) + +type private CallbackRunner = + static member Run(callback: ('T -> unit) -> 'Result, value: 'T, [] extra: int[]) : 'Result = + callback (fun received -> equal value received) + +[] +let ``test generic unit callback adaptation covers local functions and param arrays`` () = + let run (callback: ('T -> unit) -> 'Result) (value: 'T) : 'Result = + callback (fun received -> equal value received) + equal 42 (run (fun ok -> ok (); 42) ()) + equal "value" (run (fun ok -> ok 7; "value") 7) + equal 42 (CallbackRunner.Run((fun ok -> ok (); 42), (), 1, 2)) + equal "value" (CallbackRunner.Run((fun ok -> ok 7; "value"), 7)) + +[] +let ``test Async try with does not run with branch when body succeeds`` () = + let work = async { + try + return 1 + with _ -> + return 99 } + let mutable result = 0 + Async.StartWithContinuations(work, (fun r -> result <- r), ignore, ignore) + equal 1 result + +[] +let ``test Exception thrown by the continuation is not redirected to the with branch`` () = + let work = async { + try + return 1 + with _ -> + return 99 } + let mutable calls = 0 + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun _ -> calls <- calls + 1; failwith "boom from continuation"), + ignore, + ignore)) + equal 1 calls + +[] +let ``test Exception thrown by the continuation is not redirected through a bind`` () = + let work = async { + let! x = async { return 1 } + return x + 1 } + let calls = ResizeArray() + throwsAnyError (fun () -> + Async.StartWithContinuations( + work, + (fun value -> calls.Add (string value); failwith "boom from continuation"), + (fun _ -> calls.Add "error"), + (fun _ -> calls.Add "cancel"))) + equal [|"2"|] (calls.ToArray()) + +[] +let ``test FromContinuations rejects a second completion`` () = + let unitCalls = ResizeArray() + let unitWork: Async = Async.FromContinuations(fun (ok, _, _) -> ok (); ok ()) + throwsAnyError (fun () -> + Async.StartWithContinuations(unitWork, (fun () -> unitCalls.Add "success"), (fun _ -> unitCalls.Add "error"), (fun _ -> unitCalls.Add "cancel"))) + equal true (unitCalls.Count <= 1) + equal false (unitCalls.Contains "error") + equal false (unitCalls.Contains "cancel") + + let valueCalls = ResizeArray() + let valueWork = Async.FromContinuations(fun (ok, _, _) -> ok 42; ok 43) + throwsAnyError (fun () -> + Async.StartWithContinuations(valueWork, (fun v -> valueCalls.Add (string v)), (fun _ -> valueCalls.Add "error"), (fun _ -> valueCalls.Add "cancel"))) + equal true (valueCalls.Count <= 1) + equal false (valueCalls.Contains "43") + equal false (valueCalls.Contains "error") + equal false (valueCalls.Contains "cancel") diff --git a/tests/Python/TestMailboxProcessor.fs b/tests/Python/TestMailboxProcessor.fs index 6e6ce289b1..e085dbb11c 100644 --- a/tests/Python/TestMailboxProcessor.fs +++ b/tests/Python/TestMailboxProcessor.fs @@ -317,3 +317,17 @@ let ``test MailboxProcessor postAndAsyncReply can compute reply from state`` () let! sum = agent.PostAndAsyncReply(GetSum) equal 65 sum } |> Async.StartImmediate + +[] +let ``test MailboxProcessor second Reply is ignored`` () = + async { + let log = ResizeArray() + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 1 + channel.Reply 2 + log.Add "body done" }) + let! reply = agent.PostAndAsyncReply(id) + equal 1 reply + equal [|"body done"|] (log.ToArray()) + } |> Async.StartImmediate diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py index 4e04b809a1..b3cf21a3c9 100644 --- a/tests/Python/test_async_runtime.py +++ b/tests/Python/test_async_runtime.py @@ -1,9 +1,11 @@ import asyncio +from collections.abc import Callable from threading import Barrier, Condition, Event, Thread import pytest from fable_library.async_ import await_event, await_task_with_cancellation, sleep, start_with_continuations from fable_library.async_builder import ( + Async, CancellationCallbackError, CancellationToken, IAsyncContext, @@ -263,3 +265,308 @@ async def start() -> None: assert not token.listeners finally: loop.close() + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_try_finally_propagates_downstream_errors_once(channel: str) -> None: + from fable_library.async_builder import OperationCanceledError, singleton + + async def run() -> None: + cleanups = 0 + calls: list[str] = [] + trampoline = Trampoline() + token = CancellationToken() + defect = ValueError("downstream") + + def cleanup() -> None: + nonlocal cleanups + cleanups += 1 + + def terminal(value: object) -> None: + calls.append(channel) + raise defect + + def body(ctx: IAsyncContext[int]) -> None: + assert ctx.trampoline is trampoline + assert ctx.cancel_token is token + if channel == "success": + ctx.on_success(42) + elif channel == "error": + ctx.on_error(ValueError("body")) + else: + ctx.on_cancel(OperationCanceledError()) + + ctx = IAsyncContext.create(trampoline, token, terminal, terminal, terminal) + with pytest.raises(ValueError) as caught: + singleton.TryFinally(body, cleanup)(ctx) + assert caught.value is defect + assert cleanups == 1 + assert calls == [channel] + + asyncio.run(run()) + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_generic_from_continuations_propagates_terminal_exceptions(channel: str) -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations, OperationCanceledError + + async def run() -> None: + calls: list[str] = [] + defect = ValueError("downstream") + trampoline = Trampoline() + token = CancellationToken() + + def terminal(value: object) -> None: + calls.append(channel) + raise defect + + def callback(conts: Continuations[int]) -> None: + ok, error, cancelled = conts + if channel == "success": + ok(42) + elif channel == "error": + error(ValueError("body")) + else: + cancelled(OperationCanceledError()) + + ctx = IAsyncContext.create(trampoline, token, terminal, terminal, terminal) + with pytest.raises(ValueError) as caught: + from_continuations(callback)(ctx) + assert caught.value is defect + assert calls == [channel] + assert ctx.trampoline is trampoline + + asyncio.run(run()) + + +def test_generic_from_continuations_rejects_duplicate_completion() -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations + + calls: list[int] = [] + + def callback(conts: Continuations[int]) -> None: + conts[0](42) + conts[0](43) + + with pytest.raises(RuntimeError, match="invoked more than once"): + start_with_continuations(from_continuations(callback), calls.append) + assert calls == [42] + + +def test_generic_from_continuations_claims_concurrent_completion_once() -> None: + from fable_library.async_ import from_continuations + from fable_library.async_builder import Continuations + + callbacks: Continuations[int] | None = None + calls: list[str] = [] + rejected: list[Exception] = [] + barrier = Barrier(2) + + def callback(conts: Continuations[int]) -> None: + nonlocal callbacks + callbacks = conts + + start_with_continuations( + from_continuations(callback), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + ) + assert callbacks is not None + ok, error, _ = callbacks + + def complete(success: bool) -> None: + barrier.wait(5) + try: + if success: + ok(42) + else: + error(ValueError("body")) + except RuntimeError as duplicate: + rejected.append(duplicate) + + threads = [Thread(target=complete, args=(success,)) for success in (True, False)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert len(calls) == 1 + assert len(rejected) == 1 + + +@pytest.mark.parametrize("combinator", ["try_with", "bind"]) +def test_completed_continuation_exceptions_are_not_redirected(combinator: str) -> None: + from fable_library.async_builder import singleton + + calls: list[str] = [] + defect = ValueError("downstream") + + if combinator == "try_with": + work = singleton.TryWith(singleton.Return(1), lambda _: singleton.Return(99)) + else: + work = singleton.Bind(singleton.Return(1), lambda x: singleton.Return(x + 1)) + + def success(value: int) -> None: + calls.append(f"success:{value}") + raise defect + + with pytest.raises(ValueError) as caught: + start_with_continuations(work, success, lambda _: calls.append("error"), lambda _: calls.append("cancel")) + assert caught.value is defect + assert len(calls) == 1 + + +def test_workflow_errors_still_reach_the_handler_before_completion() -> None: + from fable_library.async_builder import singleton + + def failing_binder(_: int) -> Async[str]: + raise ValueError("binder") + + def handler(error: Exception) -> Async[str]: + return singleton.Return(f"handled:{error}") + + results: list[object] = [] + work = singleton.TryWith(singleton.Bind(singleton.Return(1), failing_binder), handler) + start_with_continuations(work, results.append, lambda e: results.append(("error", e))) + assert results == ["handled:binder"] + + +@pytest.mark.parametrize("channel", ["success", "error", "cancel"]) +def test_unit_from_continuations_propagates_terminal_exceptions(channel: str) -> None: + from fable_library.async_ import from_continuations_unit + from fable_library.async_builder import OperationCanceledError + + calls: list[str] = [] + defect = ValueError("downstream") + + def terminal(value: object = None) -> None: + calls.append(channel) + raise defect + + def callback( + conts: tuple[Callable[[], None], Callable[[Exception], None], Callable[[OperationCanceledError], None]], + ) -> None: + ok, error, cancelled = conts + if channel == "success": + ok() + elif channel == "error": + error(ValueError("body")) + else: + cancelled(OperationCanceledError()) + + with pytest.raises(ValueError) as caught: + start_with_continuations(from_continuations_unit(callback), terminal, terminal, terminal) + assert caught.value is defect + assert calls == [channel] + + +def test_unit_from_continuations_rejects_duplicate_completion() -> None: + from fable_library.async_ import from_continuations_unit + + calls: list[object] = [] + + def callback(conts: tuple[Callable[[], None], object, object]) -> None: + conts[0]() + conts[0]() + + with pytest.raises(RuntimeError, match="invoked more than once"): + start_with_continuations(from_continuations_unit(callback), calls.append) + assert calls == [None] + + +def test_try_finally_claims_concurrent_completion_once() -> None: + from fable_library.async_builder import OperationCanceledError, singleton + + captured: list[IAsyncContext[int]] = [] + calls: list[str] = [] + cleanups: list[int] = [] + barrier = Barrier(2) + + def body(ctx: IAsyncContext[int]) -> None: + captured.append(ctx) + + start_with_continuations( + singleton.TryFinally(body, lambda: cleanups.append(1)), + lambda _: calls.append("success"), + lambda _: calls.append("error"), + lambda _: calls.append("cancel"), + ) + assert len(captured) == 1 + ctx = captured[0] + + def complete(success: bool) -> None: + barrier.wait(5) + if success: + ctx.on_success(42) + else: + ctx.on_cancel(OperationCanceledError()) + + threads = [Thread(target=complete, args=(success,)) for success in (True, False)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert len(cleanups) == 1 + assert len(calls) == 1 + + +def test_post_and_async_reply_delivers_once_when_reply_races_with_start() -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + for _ in range(20): + channels: list[AsyncReplyChannel[int]] = [] + calls: list[object] = [] + failures: list[BaseException] = [] + barrier = Barrier(2) + mailbox: MailboxProcessor[AsyncReplyChannel[int]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int]) -> AsyncReplyChannel[int]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + assert len(channels) == 1 + + def start() -> None: + barrier.wait(5) + try: + start_with_continuations(work, calls.append, lambda e: failures.append(e), lambda e: failures.append(e)) + except BaseException as error: + failures.append(error) + + def reply() -> None: + barrier.wait(5) + try: + channels[0].reply(42) + except BaseException as error: + failures.append(error) + + threads = [Thread(target=start), Thread(target=reply)] + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + assert failures == [] + assert calls == [42] + + +def test_post_and_async_reply_ignores_a_second_reply() -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + channels: list[AsyncReplyChannel[int]] = [] + calls: list[object] = [] + mailbox: MailboxProcessor[AsyncReplyChannel[int]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int]) -> AsyncReplyChannel[int]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + channels[0].reply(1) + channels[0].reply(2) + assert calls == [1] From 87fc89f75b6ad22fafec72b4a57415682b4059df Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 22:37:31 +0200 Subject: [PATCH 175/322] fix(all): recompile dependents on signature change in watch (#5084) --- src/Fable.Cli/Main.fs | 194 ++++++++++++++---- src/Fable.Transforms/Babel/Fable2Babel.fs | 1 + src/Fable.Transforms/Beam/Fable2Beam.fs | 1 + src/Fable.Transforms/Dart/Fable2Dart.fs | 1 + src/Fable.Transforms/FSharp2Fable.Util.fs | 3 + src/Fable.Transforms/FSharp2Fable.fs | 16 +- src/Fable.Transforms/Global/Compiler.fs | 1 + src/Fable.Transforms/Php/Fable2Php.fs | 1 + src/Fable.Transforms/Python/PythonCompiler.fs | 1 + src/Fable.Transforms/Rust/Fable2Rust.fs | 1 + src/Fable.Transforms/State.fs | 151 ++++++++++++++ 11 files changed, 331 insertions(+), 40 deletions(-) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index a616e2528f..351d9ec0a3 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -220,6 +220,7 @@ module private Util = Logs = com.Logs InlineExprs = Array.empty WatchDependencies = com.WatchDependencies + SignatureDependencies = com.SignatureDependencies |} with e -> return @@ -375,11 +376,11 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir - let watchDependencies = + let watchDependencies, signatureDependencies = if cliArgs.IsWatch then - Some(HashSet()) + Some(HashSet()), Some(HashSet()) else - None + None, None CompilerImpl( currentFile, @@ -388,7 +389,8 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi fableLibDir, crackerResponse.OutputType, ?outDir = cliArgs.OutDir, - ?watchDependencies = watchDependencies + ?watchDependencies = watchDependencies, + ?signatureDependencies = signatureDependencies ) member _.MapSourceFiles(f) = @@ -440,6 +442,7 @@ type FableCompileResult = Logs: LogEntry[] InlineExprs: (string * InlineExpr)[] WatchDependencies: string[] + SignatureDependencies: string[] |}, {| File: string @@ -457,6 +460,7 @@ type FableCompilerMsg = pathResolver: PathResolver * isSilent: bool * isTriggeredByDependency: (string -> bool) * + getSignatureDependents: (string list * string list -> string[]) * ReplyChannel | FSharpFileTypeChecked of FSharpImplementationFileContents | FSharpCompilationFinished of FSharpCheckProjectResults @@ -469,8 +473,14 @@ type FableCompilerState = PathResolver: PathResolver IsSilent: bool TriggeredByDependency: string -> bool + SourceFiles: Fable.Compiler.File[] + GetSignatureDependents: string list * string list -> string[] + /// Files whose public signature changed in this cycle, and the subset whose literal values changed + SignatureChanges: string list + LiteralChanges: string list FilesToCompile: string[] FilesToCompileSet: Set + FilesCompiledInCycle: Set FilesCheckedButNotCompiled: Set FableFilesToCompileExpectedCount: int FableFilesCompiledCount: int @@ -484,15 +494,29 @@ type FableCompilerState = } static member Create - (fableProj, filesToCompile: string[], ?pathResolver, ?isSilent, ?triggeredByDependency, ?replyChannel) + ( + fableProj, + filesToCompile: string[], + ?sourceFiles, + ?pathResolver, + ?isSilent, + ?triggeredByDependency, + ?getSignatureDependents, + ?replyChannel + ) = { FableProj = fableProj PathResolver = defaultArg pathResolver PathResolver.Dummy IsSilent = defaultArg isSilent false TriggeredByDependency = defaultArg triggeredByDependency (fun _ -> false) + SourceFiles = defaultArg sourceFiles [||] + GetSignatureDependents = defaultArg getSignatureDependents (fun _ -> [||]) + SignatureChanges = [] + LiteralChanges = [] FilesToCompile = filesToCompile FilesToCompileSet = set filesToCompile + FilesCompiledInCycle = set filesToCompile FilesCheckedButNotCompiled = Set.empty FableFilesToCompileExpectedCount = filesToCompile |> Array.filter isImplementationFile |> Array.length FableFilesCompiledCount = 0 @@ -517,6 +541,54 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let startInThreadPool toMsg work = postTo toMsg work |> Async.Start + let subscriber = + if projCracked.CliArgs.NoParallelTypeCheck then + None + else + Some(FSharpFileTypeChecked >> agent.Post) + + let typeCheck (sourceFiles: Fable.Compiler.File[]) (lastFile: string) = + startInThreadPool + FSharpCompilationFinished + (fun () -> + let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader sourceFiles + + checker.ParseAndCheckProject( + projCracked.ProjectFile, + filePaths, + sourceReader, + lastFile, + ?subscriber = subscriber + ) + ) + + let updateProject (state: FableCompilerState) (files: FSharpImplementationFileContents list) = + let fableProj = state.FableProj.Update(files) + + let signatureChanges, literalChanges = + ((state.SignatureChanges, state.LiteralChanges), files) + ||> List.fold (fun (signatureChanges, literalChanges) file -> + let fileName = Path.normalizePathAndEnsureFsExtension file.FileName + + match + Map.tryFind fileName state.FableProj.ImplementationFiles, + Map.tryFind fileName fableProj.ImplementationFiles + with + | Some before, Some after when FileSignature.Changed(before.Signature, after.Signature) -> + fileName :: signatureChanges, + (if FileSignature.LiteralsChanged(before.Signature, after.Signature) then + fileName :: literalChanges + else + literalChanges) + | _ -> signatureChanges, literalChanges + ) + + { state with + FableProj = fableProj + SignatureChanges = signatureChanges + LiteralChanges = literalChanges + } + let fableCompile state fileName = let fableProj = state.FableProj @@ -562,37 +634,21 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, replyChannel) -> let state = FableCompilerState.Create( state.FableProj, filesToCompile, + sourceFiles, pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, replyChannel ) - startInThreadPool - FSharpCompilationFinished - (fun () -> - let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader sourceFiles - - let subscriber = - if projCracked.CliArgs.NoParallelTypeCheck then - None - else - Some(FSharpFileTypeChecked >> agent.Post) - - checker.ParseAndCheckProject( - projCracked.ProjectFile, - filePaths, - sourceReader, - Array.last filesToCompile, - ?subscriber = subscriber - ) - ) - + typeCheck sourceFiles (Array.last filesToCompile) return! loop state | FSharpFileTypeChecked file -> @@ -613,7 +669,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl if not (state.FilesToCompileSet.Contains(fileName)) then state else - let state = { state with FableProj = state.FableProj.Update([ file ]) } + let state = updateProject state [ file ] // For Rust the last file has to compile after every other // one: it re-exports the namespaces the other files @@ -644,7 +700,10 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let state = { state with - FSharpLogs = CodeServices.getFSharpDiagnostics results.Diagnostics + FSharpLogs = + Array.append + state.FSharpLogs + (CodeServices.getFSharpDiagnostics results.Diagnostics) HasFSharpCompilationFinished = true } @@ -656,7 +715,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl else results.AssemblyContents.ImplementationFiles - let state = { state with FableProj = state.FableProj.Update(implFiles) } + let state = updateProject state (List.ofSeq implFiles) let filesToCompile = state.FilesToCompile @@ -669,8 +728,37 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl else state - FableCompiler.CheckIfCompilationIsFinished(state) - return! loop state + let dependents = + if List.isEmpty state.SignatureChanges then + [||] + else + state.GetSignatureDependents(state.SignatureChanges, state.LiteralChanges) + |> Array.filter (fun file -> not (state.FilesCompiledInCycle.Contains file)) + + if Array.isEmpty dependents then + FableCompiler.CheckIfCompilationIsFinished(state) + return! loop state + else + Log.verbose ( + lazy + $"""Signature changed in {String.concat ", " (state.SignatureChanges |> List.map File.relPathToCurDir)}, recompiling {dependents.Length} dependent files""" + ) + + let state = + { state with + SignatureChanges = [] + LiteralChanges = [] + FilesToCompile = dependents + FilesToCompileSet = set dependents + FilesCompiledInCycle = Set.union state.FilesCompiledInCycle (set dependents) + FableFilesToCompileExpectedCount = + state.FableFilesToCompileExpectedCount + + (dependents |> Array.filter isImplementationFile |> Array.length) + HasFSharpCompilationFinished = false + } + + typeCheck state.SourceFiles (Array.last dependents) + return! loop state | FableFileCompiled(fileName, result) -> let state = @@ -747,7 +835,9 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl member _.GetFableProject() = agent.PostAndAsyncReply(GetFableProject) - member _.StartCompilation(sourceFiles, filesToCompile, pathResolver, isSilent, isTriggeredByDependency) = + member _.StartCompilation + (sourceFiles, filesToCompile, pathResolver, isSilent, isTriggeredByDependency, getSignatureDependents) + = async { if Array.isEmpty filesToCompile then return [||], [] @@ -765,6 +855,7 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl pathResolver, isSilent, isTriggeredByDependency, + getSignatureDependents, channel ) ) @@ -865,6 +956,7 @@ type State = CliArgs: CliArgs ProjectCrackedAndFableCompiler: (ProjectCracked * FableCompiler) option WatchDependencies: Map + SignatureDependencies: Map PendingFiles: string[] DeduplicateDic: ConcurrentDictionary Watcher: Watcher option @@ -900,6 +992,7 @@ type State = CliArgs = cliArgs ProjectCrackedAndFableCompiler = None WatchDependencies = Map.empty + SignatureDependencies = Map.empty Watcher = watchDelay |> Option.map Watcher.Create DeduplicateDic = ConcurrentDictionary() PendingFiles = [||] @@ -1449,23 +1542,49 @@ let private compilationCycle (state: State) (changes: ISet) = | None -> FableCompiler.Init(projCracked) | Some fableCompiler -> async.Return fableCompiler + // Literal values are inlined by the F# compiler, so a changed literal file is followed by every later file + let getSignatureDependents (changedFiles: string list, literalFiles: string list) = + let changedFiles = set changedFiles + + let firstLiteralFile = + literalFiles + |> List.choose (fun file -> Array.tryFindIndex ((=) file) projCracked.SourceFilePaths) + |> List.sort + |> List.tryHead + + projCracked.SourceFilePaths + |> Array.indexed + |> Array.filter (fun (index, path) -> + (match Map.tryFind path state.SignatureDependencies with + | Some dependencies -> dependencies |> Array.exists changedFiles.Contains + | None -> false) + || ( + match firstLiteralFile with + | Some literalIndex -> index > literalIndex + | None -> false + ) + ) + |> Array.map snd + let! fsharpLogs, fableResults = fableCompiler.StartCompilation( projCracked.SourceFiles, // Make sure to pass the up-to-date source files (with cleared hashes for changed files) filesToCompile, state.GetPathResolver(?precompiledInfo = projCracked.PrecompiledInfo), state.SilentCompilation, - fun f -> state.TriggeredByDependency(f, changes) + (fun f -> state.TriggeredByDependency(f, changes)), + getSignatureDependents ) - let logs, watchDependencies = - ((fsharpLogs, state.WatchDependencies), fableResults) - ||> List.fold (fun (logs, deps) -> + let logs, watchDependencies, signatureDependencies = + ((fsharpLogs, state.WatchDependencies, state.SignatureDependencies), fableResults) + ||> List.fold (fun (logs, deps, sigDeps) -> function | Ok res -> let logs = Array.append logs res.Logs let deps = Map.add res.File res.WatchDependencies deps - logs, deps + let sigDeps = Map.add res.File res.SignatureDependencies sigDeps + logs, deps, sigDeps | Error e -> let log = match e.Exception with @@ -1475,13 +1594,14 @@ let private compilationCycle (state: State) (changes: ISet) = LogEntry.MakeError(msg, fileName = e.File, tag = "EXCEPTION") - Array.append logs [| log |], deps + Array.append logs [| log |], deps, sigDeps ) let state = { state with PendingFiles = [||] WatchDependencies = watchDependencies + SignatureDependencies = signatureDependencies SilentCompilation = false } diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index f72d32e3a7..f3a630f375 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -5059,6 +5059,7 @@ module Compiler = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 9ee0cf6453..73a6506d27 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -3842,6 +3842,7 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = member _.TryGetEntity(ref) = com.TryGetEntity(ref) member _.GetInlineExpr(key) = com.GetInlineExpr(key) member _.AddWatchDependency(file) = com.AddWatchDependency(file) + member _.AddSignatureDependency(file) = com.AddSignatureDependency(file) member _.AddLog(msg, severity, ?range, ?fileName, ?tag) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index f5e9570c08..d590902545 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -3208,6 +3208,7 @@ module Compiler = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index cd84edd7dd..c4de960e19 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -2432,6 +2432,7 @@ module Util = if not com.IsPrecompilingInlineFunction && file = com.CurrentFile then makeIdentExpr entityName else + com.AddSignatureDependency(file) makeInternalClassImport com ent entityName file let entityIdent (com: Compiler) (ent: Fable.EntityRef) = entityIdentWithSuffix com ent "" @@ -2514,6 +2515,8 @@ module Util = if hasOverloadSuffix then com.AddWatchDependency(file) + com.AddSignatureDependency(file) + // Private values are not exported so they can't be imported by call sites in other files. // We need to handle it manually because Fable handle resolve inline function itself if diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index db18d095fb..b31c8b1a95 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -813,6 +813,14 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.Const(value, typ) -> let typ = makeType ctx.GenericArgs typ + + // Enum cases reach Fable as their numeric value, so files using them must follow the enum declaration + do + match typ with + | Fable.Number(_, Fable.NumberInfo.IsEnum entRef) -> + entRef.SourcePath |> Option.iter com.AddSignatureDependency + | _ -> () + let expr = makeTypeConst (makeRangeFrom fsExpr) typ value return expr @@ -1485,10 +1493,11 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs let! argExprs = transformExprList com ctx argExprs let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) + let entRef = FsEnt.Ref fsType.TypeDefinition + // Record fields are passed by position, so a field reordering in the declaring file changes this call + entRef.SourcePath |> Option.iter com.AddSignatureDependency - return - Fable.NewRecord(argExprs, FsEnt.Ref fsType.TypeDefinition, genArgs) - |> makeValue r + return Fable.NewRecord(argExprs, entRef, genArgs) |> makeValue r | FSharpExprPatterns.NewAnonRecord(fsType, argExprs) -> let r = makeRangeFrom fsExpr @@ -2947,6 +2956,7 @@ type FableCompiler(com: Compiler) = member _.TryGetEntity(entRef) = com.TryGetEntity(entRef) member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index b54de2efec..a009edd56c 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -87,6 +87,7 @@ type Compiler = abstract TryGetEntity: Fable.EntityRef -> Fable.Entity option abstract GetInlineExpr: string -> InlineExpr abstract AddWatchDependency: file: string -> unit + abstract AddSignatureDependency: file: string -> unit abstract AddLog: msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string -> unit diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 4afbb5ee5b..278f7f8c92 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -2142,6 +2142,7 @@ type PhpCompiler(com: Fable.Compiler) = com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag) member this.AddWatchDependency(file) = com.AddWatchDependency(file) + member this.AddSignatureDependency(file) = com.AddSignatureDependency(file) member this.GetImplementationFile(fileName) = com.GetImplementationFile(fileName) diff --git a/src/Fable.Transforms/Python/PythonCompiler.fs b/src/Fable.Transforms/Python/PythonCompiler.fs index 63563c02bf..3b1f062230 100644 --- a/src/Fable.Transforms/Python/PythonCompiler.fs +++ b/src/Fable.Transforms/Python/PythonCompiler.fs @@ -136,6 +136,7 @@ type PythonCompiler(com: Compiler) = member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index a904a4efd2..4bcf1bbe0c 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -6439,6 +6439,7 @@ module Compiler = member _.TryGetEntity(entRef) = com.TryGetEntity(entRef) member _.GetInlineExpr(fullName) = com.GetInlineExpr(fullName) member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) + member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index fc414d6894..4ab442f112 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -135,6 +135,53 @@ type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severi member _.Plugins = plugins +type ParameterSignature = + { + Name: string option + IsOptional: bool + IsParamArray: bool + } + +type MemberSignature = + { + Type: Fable.Type + Parameters: ParameterSignature list list + IsInline: FSharpInlineAnnotation + IsMutable: bool + Attributes: (string * obj list) list + } + +type EntitySignature = + { + Attributes: (string * obj list) list + Kind: bool * bool * bool * bool * bool + Fields: (string * Fable.Type * obj) list + UnionCases: (string * (string * Fable.Type) list) list + } + +/// The parts of a file other files can depend on without naming them in their own typed tree +type FileSignature = + { + IsUnknown: bool + Literals: Map + Members: Map + Entities: Map + } + + static member Unknown = + { + IsUnknown = true + Literals = Map.empty + Members = Map.empty + Entities = Map.empty + } + + static member Changed(before: FileSignature, after: FileSignature) = + before.IsUnknown || after.IsUnknown || before <> after + + static member LiteralsChanged(before: FileSignature, after: FileSignature) = + before.IsUnknown || after.IsUnknown || before.Literals <> after.Literals + type ImplFile = { Declarations: FSharpImplementationFileDeclaration list @@ -142,6 +189,7 @@ type ImplFile = RootComment: FSharpXmlDoc option Entities: IReadOnlyDictionary InlineExprs: (string * InlineExprLazy) list + Signature: FileSignature } static member From(file: FSharpImplementationFileContents) = @@ -154,6 +202,96 @@ type ImplFile = loop entities e.NestedEntities + let signature (declarations: FSharpImplementationFileDeclaration list) = + try + let makeType = FSharp2Fable.TypeHelpers.makeType Map.empty + + let attributes (atts: FSharpAttribute seq) = + atts + |> Seq.map (fun att -> + FSharp2Fable.FsEnt.FullName att.AttributeType, + att.ConstructorArguments |> Seq.map snd |> List.ofSeq + ) + |> List.ofSeq + + let rec members (decls: FSharpImplementationFileDeclaration list) = + decls + |> List.collect ( + function + | FSharpImplementationFileDeclaration.Entity(_, decls) -> members decls + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, _, _) when + not memb.IsCompilerGenerated && not memb.Accessibility.IsPrivate + -> + [ memb ] + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue _ + | FSharpImplementationFileDeclaration.InitAction _ -> [] + ) + + let rec entities (ents: FSharpEntity seq) = + ents |> Seq.collect (fun e -> Seq.append [ e ] (entities e.NestedEntities)) + + let members = members declarations + + { + IsUnknown = false + Literals = + members + |> List.choose (fun memb -> + memb.LiteralValue + |> Option.map (fun value -> FSharp2Fable.Helpers.getMemberUniqueName memb, value) + ) + |> Map + Members = + members + |> List.map (fun memb -> + FSharp2Fable.Helpers.getMemberUniqueName memb, + { + Type = makeType memb.FullType + Parameters = + memb.CurriedParameterGroups + |> Seq.map ( + Seq.map (fun p -> + { + Name = p.Name + IsOptional = p.IsOptionalArg + IsParamArray = p.IsParamArrayArg + } + ) + >> List.ofSeq + ) + |> List.ofSeq + IsInline = memb.InlineAnnotation + IsMutable = memb.IsMutable + Attributes = attributes memb.Attributes + } + ) + |> Map + Entities = + FSharp2Fable.Compiler.getRootFSharpEntities declarations + |> entities + |> Seq.map (fun e -> + FSharp2Fable.FsEnt.FullName e, + { + Attributes = attributes e.Attributes + Kind = e.IsFSharpRecord, e.IsFSharpUnion, e.IsEnum, e.IsInterface, e.IsValueType + Fields = + e.FSharpFields + |> Seq.map (fun f -> f.Name, makeType f.FieldType, Option.toObj f.LiteralValue) + |> List.ofSeq + UnionCases = + e.UnionCases + |> Seq.map (fun c -> + c.Name, + c.Fields |> Seq.map (fun f -> f.Name, makeType f.FieldType) |> List.ofSeq + ) + |> List.ofSeq + } + ) + |> Map + } + with _ -> + FileSignature.Unknown + // add all entities to the entity cache let entities = Dictionary() @@ -172,6 +310,7 @@ type ImplFile = RootModule = rootModule RootComment = rootComment InlineExprs = FSharp2Fable.Compiler.getInlineExprs file.FileName declarations + Signature = signature declarations } type PrecompiledInfo = @@ -319,6 +458,7 @@ type CompilerImpl ?outType: OutputType, ?outDir: string, ?watchDependencies: HashSet, + ?signatureDependencies: HashSet, ?logs: ResizeArray, ?isPrecompilingInlineFunction: bool ) @@ -336,6 +476,11 @@ type CompilerImpl | Some w -> Array.ofSeq w | None -> [||] + member _.SignatureDependencies = + match signatureDependencies with + | Some w -> Array.ofSeq w + | None -> [||] + interface Compiler with member _.Options = options member _.Plugins = project.Assemblies.Plugins @@ -369,6 +514,7 @@ type CompilerImpl outType, ?outDir = outDir, ?watchDependencies = watchDependencies, + ?signatureDependencies = signatureDependencies, logs = logs, isPrecompilingInlineFunction = true ) @@ -429,6 +575,11 @@ type CompilerImpl | Some watchDependencies when file <> currentFile -> watchDependencies.Add(file) |> ignore | _ -> () + member _.AddSignatureDependency(file) = + match signatureDependencies with + | Some signatureDependencies when file <> currentFile -> signatureDependencies.Add(file) |> ignore + | _ -> () + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag) |> logs.Add From f19e2c56566488dfeb48e71508bdb218aaa58582 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Thu, 15 Oct 2026 22:45:11 +0200 Subject: [PATCH 176/322] fix(python): preserve reusable replies and unit continuations (#5082) --- .../Python/Fable2Python.Util.fs | 5 +-- src/fable-library-py/fable_library/async_.py | 7 ++- .../fable_library/mailbox_processor.py | 8 ++-- tests/Js/Main/AsyncTests.fs | 17 ++++++- tests/Python/TestAsync.fs | 10 +++++ tests/Python/TestMailboxProcessor.fs | 20 ++++++++- tests/Python/test_async_runtime.py | 45 +++++++++++++++++++ 7 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 40fe337adb..566f277627 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -329,12 +329,11 @@ module Util = && List.exists2 mismatches expectedArgs actualArgs | _ -> false + // Generic-to-unit callback inputs need the typing adapter both directly and inside callbacks. expectedType |> Option.exists (fun expected -> match FableTransforms.uncurryType expected, FableTransforms.uncurryType argExpr.Type with - | Fable.DelegateType(expectedArgs, _), Fable.DelegateType(actualArgs, _) -> - List.sameLength expectedArgs actualArgs - && List.exists2 mismatches expectedArgs actualArgs + | Fable.DelegateType _, Fable.DelegateType _ -> mismatches expected argExpr.Type | _ -> false ) diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 4f82c01e59..7f08e65750 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -275,7 +275,12 @@ def from_continuations_unit( ) -> Async[None]: def callback(conts: Continuations[None]) -> None: success, error, cancelled = conts - f((lambda: success(None), error, cancelled)) + + # Generic callers pass None explicitly; specialized F# unit callers erase it. + def unit_success(value: None = None) -> None: + success(value) + + f((unit_success, error, cancelled)) return from_continuations(callback) diff --git a/src/fable-library-py/fable_library/mailbox_processor.py b/src/fable-library-py/fable_library/mailbox_processor.py index ef8bc9e734..5762d22f28 100644 --- a/src/fable-library-py/fable_library/mailbox_processor.py +++ b/src/fable-library-py/fable_library/mailbox_processor.py @@ -73,20 +73,20 @@ def post_and_async_reply[Reply](self, build_message: Callable[[AsyncReplyChannel # Track whether `reply` was actually called, since the reply value itself # may legitimately be `None` (e.g. `AsyncReplyChannel[None]` for unit). replied = False - delivered = False gate = Lock() continuation: Continuations[Any] | None = ( None # This is the continuation for the `done` callback of the awaiting poster. ) def check_completion() -> None: - nonlocal delivered + nonlocal continuation # reply may run on another thread before the poster's continuation is installed with gate: - if delivered or not replied or continuation is None: + if not replied or continuation is None: return - delivered = True deliver = continuation[0] + # Consume this waiter once, retaining the reply for later executions. + continuation = None deliver(result) def reply_callback(res: Reply): diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index fc17756520..2ca07f8b86 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -853,16 +853,31 @@ let tests = testCaseAsync "MailboxProcessor second Reply is ignored" <| fun () -> async { let log = ResizeArray() + let completed = new MailboxProcessor(fun _ -> async { return () }) let agent = MailboxProcessor>.Start(fun inbox -> async { let! channel = inbox.Receive() channel.Reply 1 channel.Reply 2 - log.Add "body done" }) + log.Add "body done" + completed.Post() }) let! reply = agent.PostAndAsyncReply(id) equal 1 reply + do! completed.Receive() equal [|"body done"|] (log.ToArray()) } + testCaseAsync "MailboxProcessor reply computation can be awaited twice" <| fun () -> + async { + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 42 }) + let work = agent.PostAndAsyncReply(id) + let! first = work + let! second = work + equal 42 first + equal 42 second + } + testCase "Async try .. with returns correctly from 'with' branch" <| fun () -> let work = async { try diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index c41e74086c..b2c1755ecd 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -1093,6 +1093,16 @@ let ``test TryFinally propagates downstream continuation errors once`` () = let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (OperationCanceledException -> unit) -> unit) : Async<'T> = Async.FromContinuations(fun (ok, error, cancelled) -> callback ok error cancelled) +let private forwardGenericValue (continuation: 'T -> unit) (value: 'T) = + continuation value + +[] +let ``test unit FromContinuations accepts a forwarded generic value`` () = + let work: Async = Async.FromContinuations(fun (ok, _, _) -> forwardGenericValue ok ()) + let mutable completed = false + Async.StartWithContinuations(work, (fun () -> completed <- true), raise, raise) + equal true completed + [] let ``test generic FromContinuations accepts erased unit callbacks`` () = let calls = ResizeArray() diff --git a/tests/Python/TestMailboxProcessor.fs b/tests/Python/TestMailboxProcessor.fs index e085dbb11c..9eeac160b2 100644 --- a/tests/Python/TestMailboxProcessor.fs +++ b/tests/Python/TestMailboxProcessor.fs @@ -322,12 +322,28 @@ let ``test MailboxProcessor postAndAsyncReply can compute reply from state`` () let ``test MailboxProcessor second Reply is ignored`` () = async { let log = ResizeArray() + let completed = new MailboxProcessor(fun _ -> async { return () }) let agent = MailboxProcessor>.Start(fun inbox -> async { let! channel = inbox.Receive() channel.Reply 1 channel.Reply 2 - log.Add "body done" }) + log.Add "body done" + completed.Post() }) let! reply = agent.PostAndAsyncReply(id) equal 1 reply + do! completed.Receive() equal [|"body done"|] (log.ToArray()) - } |> Async.StartImmediate + } |> Async.RunSynchronously + +[] +let ``test MailboxProcessor reply computation can be awaited twice`` () = + async { + let agent = MailboxProcessor>.Start(fun inbox -> async { + let! channel = inbox.Receive() + channel.Reply 42 }) + let work = agent.PostAndAsyncReply(id) + let! first = work + let! second = work + equal 42 first + equal 42 second + } |> Async.RunSynchronously diff --git a/tests/Python/test_async_runtime.py b/tests/Python/test_async_runtime.py index b3cf21a3c9..72ad4a3ccb 100644 --- a/tests/Python/test_async_runtime.py +++ b/tests/Python/test_async_runtime.py @@ -462,6 +462,22 @@ def callback( assert calls == [channel] +@pytest.mark.parametrize("explicit_unit", [False, True]) +def test_unit_from_continuations_accepts_erased_and_explicit_unit(explicit_unit: bool) -> None: + from fable_library.async_ import from_continuations_unit + + calls: list[object] = [] + + def callback(conts: tuple[Callable[..., None], object, object]) -> None: + if explicit_unit: + conts[0](None) + else: + conts[0]() + + start_with_continuations(from_continuations_unit(callback), calls.append, lambda e: calls.append(("error", e))) + assert calls == [None] + + def test_unit_from_continuations_rejects_duplicate_completion() -> None: from fable_library.async_ import from_continuations_unit @@ -552,6 +568,35 @@ def reply() -> None: assert all(not thread.is_alive() for thread in threads) assert failures == [] assert calls == [42] + start_with_continuations(work, calls.append, lambda e: failures.append(e), lambda e: failures.append(e)) + assert failures == [] + assert calls == [42, 42] + + +@pytest.mark.parametrize("value", [None, 42]) +@pytest.mark.parametrize("reply_before_start", [False, True]) +def test_post_and_async_reply_can_be_reused(value: int | None, reply_before_start: bool) -> None: + from fable_library.mailbox_processor import AsyncReplyChannel, MailboxProcessor + + channels: list[AsyncReplyChannel[int | None]] = [] + calls: list[object] = [] + mailbox: MailboxProcessor[AsyncReplyChannel[int | None]] = MailboxProcessor(lambda _: lambda _: None) + + def build(channel: AsyncReplyChannel[int | None]) -> AsyncReplyChannel[int | None]: + channels.append(channel) + return channel + + work = mailbox.post_and_async_reply(build) + if reply_before_start: + channels[0].reply(value) + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + if not reply_before_start: + channels[0].reply(value) + assert calls == [value] + + start_with_continuations(work, calls.append, lambda e: calls.append(("error", e))) + assert calls == [value, value] + assert len(channels) == 1 def test_post_and_async_reply_ignores_a_second_reply() -> None: From 50129598a13d4ab7a6e50ae1be0d9aa9d3c68ef8 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 22:52:22 +0200 Subject: [PATCH 177/322] fix(all): forward `--targetprofile` for precompile (#5085) --- src/Fable.Compiler/ProjectCracker.fs | 3 +++ tests/Integration/Integration/data/precompiledLib/App.fs | 1 + .../Integration/data/precompiledLib/App.jsx.expected | 5 ++++- .../Integration/data/precompiledLib/library/Library.fs | 7 +++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index 67d212cc50..9cf5b403ec 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -463,6 +463,9 @@ let private extractUsefulOptionsAndSources // Forward pathmap from the main project so CallerFilePath respects / elif line.StartsWith("--pathmap:", StringComparison.Ordinal) && isMainProj then accSources, line :: accOptions + // FCS defaults the primary assembly to mscorlib, a facade in the .NET ref pack, which breaks IL-erased union helpers in precompiled assemblies + elif line.StartsWith("--targetprofile:", StringComparison.Ordinal) && isMainProj then + accSources, line :: accOptions elif line.StartsWith("--nowarn", StringComparison.Ordinal) || line.StartsWith("--warnon", StringComparison.Ordinal) diff --git a/tests/Integration/Integration/data/precompiledLib/App.fs b/tests/Integration/Integration/data/precompiledLib/App.fs index 9730e7add9..a5fbc031f7 100644 --- a/tests/Integration/Integration/data/precompiledLib/App.fs +++ b/tests/Integration/Integration/data/precompiledLib/App.fs @@ -3,3 +3,4 @@ module App let greeting = Library.greeting "world" let doubled = Library.double 21 let plain = Library.plain 1 +let circles = Library.circles [ Library.Circle 1.0; Library.Square 2.0 ] diff --git a/tests/Integration/Integration/data/precompiledLib/App.jsx.expected b/tests/Integration/Integration/data/precompiledLib/App.jsx.expected index c58d20b05d..48ca2adfd8 100644 --- a/tests/Integration/Integration/data/precompiledLib/App.jsx.expected +++ b/tests/Integration/Integration/data/precompiledLib/App.jsx.expected @@ -1,5 +1,6 @@ -import { plain as plain_1 } from "./precompiled/Library.js"; +import { Shape, circles as circles_1, plain as plain_1 } from "./precompiled/Library.js"; +import { ofArray } from "./precompiled/fable_modules/fable-library-js.5.21.0/List.js"; export const greeting = { Greet(helpers) { @@ -11,3 +12,5 @@ export const doubled = 21 * 2; export const plain = plain_1(1); +export const circles = circles_1(ofArray([new Shape(/* Circle */ 0, [1]), new Shape(/* Square */ 1, [2])])); + diff --git a/tests/Integration/Integration/data/precompiledLib/library/Library.fs b/tests/Integration/Integration/data/precompiledLib/library/Library.fs index dc2e64a93f..fbada775bb 100644 --- a/tests/Integration/Integration/data/precompiledLib/library/Library.fs +++ b/tests/Integration/Integration/data/precompiledLib/library/Library.fs @@ -15,3 +15,10 @@ let inline greeting (name: string) = let inline double (value: int) = value * 2 let plain (value: int) = value + 1 + +type Shape = + | Circle of float + | Square of float + +/// Regression shape: F# 9 union case testers, which the F# compiler emits as IL helpers +let circles (shapes: Shape list) = shapes |> List.filter (fun shape -> shape.IsCircle) From 07989b16214c4fcf8e33318fba1dd3f77df77cbb Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 23:21:41 +0200 Subject: [PATCH 178/322] fix(python): emit widening integer casts (#5087) Co-authored-by: Dag Brattli --- .../Python/Fable2Python.Transforms.fs | 8 ++++++++ tests/Python/TestConvert.fs | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index c445abfde4..180e9855e1 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -382,6 +382,14 @@ let transformCast (com: IPythonCompiler) (ctx: Context) t e : Expression * State let value, stmts = com.TransformAsExpr(ctx, e) libCall com ctx None "core" "int32" [ value ], stmts | _ -> com.TransformAsExpr(ctx, e) + // The other integer widths are wrapper classes too, so `uint32 (x: byte)` needs the same explicit constructor call + | Fable.Number((Int8 | UInt8 | Int16 | UInt16 | UInt32) as kind, _), _ -> + match e.Type with + | Fable.Number(sourceKind, _) when sourceKind = kind -> com.TransformAsExpr(ctx, e) + | Fable.Number _ -> + let value, stmts = com.TransformAsExpr(ctx, e) + libCall com ctx None "core" (Py.Replacements.getIntTypeName kind) [ 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 diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index 781b711289..a77a50fcaf 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -418,6 +418,26 @@ let private toIntFromInt16 (x: int16) = int x let private toFloatFromSingle (x: float32) = float x let private toFloatFromByte (x: byte) = float x let private toFloatFromInt (x: int) = float x +let private toInt16FromSByte (x: sbyte) = int16 x +let private toUInt16FromByte (x: byte) = uint16 x +let private toUInt32FromByte (x: byte) = uint32 x +let private toUInt32FromUInt16 (x: uint16) = uint32 x + +[] +let ``test Widening between integer wrappers uses the target width`` () = + // Compare as int so wrapper equality cannot truncate the expected value to the source width. + toInt16FromSByte 100y <<< 1 |> int |> equal 200 + toInt16FromSByte -100y <<< 1 |> int |> equal -200 + toUInt16FromByte 200uy <<< 1 |> int |> equal 400 + toUInt32FromByte 200uy <<< 1 |> int |> equal 400 + toUInt32FromUInt16 60000us <<< 1 |> int |> equal 120000 + +[] +let ``test Widening between integer wrappers has the target runtime type`` () = + box (toInt16FromSByte 5y) :? int16 |> equal true + box (toUInt16FromByte 5uy) :? uint16 |> equal true + box (toUInt32FromByte 5uy) :? uint32 |> equal true + box (toUInt32FromUInt16 5us) :? uint32 |> equal true [] let ``test Widening to int does not keep the source width`` () = From 6b995913acbe98434f252b19b07182cc831a8a0f Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 15 Oct 2026 23:44:04 +0200 Subject: [PATCH 179/322] feat(standalone)!: return tagged tooltips instead of text (#5088) --- src/fable-standalone/CHANGELOG.md | 1 + src/fable-standalone/src/Interfaces.fs | 57 ++++++++++- src/fable-standalone/src/Main.fs | 105 ++++++++++++++++----- src/fable-standalone/src/Worker/Shared.fs | 2 +- src/fable-standalone/src/Worker/Worker.fs | 12 +-- src/fable-standalone/test/worker/README.md | 3 + src/fable-standalone/test/worker/run.mjs | 26 +++++ 7 files changed, 174 insertions(+), 32 deletions(-) diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index c3601aad4b..0d2b565998 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,6 @@ --- last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +pre_release: beta include: - ../Fable.Transforms/ - ../fcs-fable/ diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 311e9ac28b..f901f73536 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -43,6 +43,61 @@ type Completion = Glyph: Glyph } +/// Mirrors FCS `FSharp.Compiler.Text.TextTag` +[] +type TextTag = + | ActivePatternCase + | ActivePatternResult + | Alias + | Class + | Union + | UnionCase + | Delegate + | Enum + | Event + | Field + | Interface + | Keyword + | LineBreak + | Local + | Record + | RecordField + | Method + | Member + | ModuleBinding + | Function + | Module + | Namespace + | NumericLiteral + | Operator + | Parameter + | Property + | Space + | StringLiteral + | Struct + | TypeParameter + | Text + | Punctuation + | UnknownType + | UnknownEntity + +type TaggedText = + { + Tag: TextTag + Text: string + } + +/// One signature with its documentation. Overloads come as separate tooltips. +type ToolTip = + { + ParamName: string option + Signature: TaggedText[] + /// XML doc lines, with the implicit `` already inserted + XmlDoc: string[] + TypeMapping: TaggedText[][] + Remarks: TaggedText[] + } + type SourceMapping = int * int * int * int * string option type IChecker = interface end @@ -106,7 +161,7 @@ type IFableManager = abstract GetDeclarationLocation: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Range option - abstract GetToolTipText: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> string[] + abstract GetToolTips: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ToolTip[] abstract GetCompletionsAtLocation: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Completion[] diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index d1c0b7097c..522dd225db 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -262,31 +262,88 @@ let parseAndCheckFileInProject ParseAndCheckResults(project, Some results, Some checkResults, projectResults, otherFSharpOptions) -let tooltipToString (el: ToolTipElement) : string[] = - let dataToString (data: ToolTipElementData) = - let toString (tts: FSharp.Compiler.Text.TaggedText[]) = - tts |> Array.map (fun x -> x.Text) |> String.concat " " +type private FcsTextTag = FSharp.Compiler.Text.TextTag + +let private toTextTag (tag: FcsTextTag) = + match tag with + | FcsTextTag.ActivePatternCase -> TextTag.ActivePatternCase + | FcsTextTag.ActivePatternResult -> TextTag.ActivePatternResult + | FcsTextTag.Alias -> TextTag.Alias + | FcsTextTag.Class -> TextTag.Class + | FcsTextTag.Union -> TextTag.Union + | FcsTextTag.UnionCase -> TextTag.UnionCase + | FcsTextTag.Delegate -> TextTag.Delegate + | FcsTextTag.Enum -> TextTag.Enum + | FcsTextTag.Event -> TextTag.Event + | FcsTextTag.Field -> TextTag.Field + | FcsTextTag.Interface -> TextTag.Interface + | FcsTextTag.Keyword -> TextTag.Keyword + | FcsTextTag.LineBreak -> TextTag.LineBreak + | FcsTextTag.Local -> TextTag.Local + | FcsTextTag.Record -> TextTag.Record + | FcsTextTag.RecordField -> TextTag.RecordField + | FcsTextTag.Method -> TextTag.Method + | FcsTextTag.Member -> TextTag.Member + | FcsTextTag.ModuleBinding -> TextTag.ModuleBinding + | FcsTextTag.Function -> TextTag.Function + | FcsTextTag.Module -> TextTag.Module + | FcsTextTag.Namespace -> TextTag.Namespace + | FcsTextTag.NumericLiteral -> TextTag.NumericLiteral + | FcsTextTag.Operator -> TextTag.Operator + | FcsTextTag.Parameter -> TextTag.Parameter + | FcsTextTag.Property -> TextTag.Property + | FcsTextTag.Space -> TextTag.Space + | FcsTextTag.StringLiteral -> TextTag.StringLiteral + | FcsTextTag.Struct -> TextTag.Struct + | FcsTextTag.TypeParameter -> TextTag.TypeParameter + | FcsTextTag.Text -> TextTag.Text + | FcsTextTag.Punctuation -> TextTag.Punctuation + | FcsTextTag.UnknownType -> TextTag.UnknownType + | FcsTextTag.UnknownEntity -> TextTag.UnknownEntity + +let private toTaggedText (tts: FSharp.Compiler.Text.TaggedText[]) : TaggedText[] = + tts + |> Array.map (fun x -> + { + Tag = toTextTag x.Tag + Text = x.Text + } + ) - [| - match data.ParamName with - | Some x -> yield x + ": " - | None -> () - yield data.MainDescription |> toString - match data.XmlDoc with - | FSharp.Compiler.Symbols.FSharpXmlDoc.FromXmlText xmlDoc -> - yield! xmlDoc.UnprocessedLines - yield! xmlDoc.GetElaboratedXmlLines() - | _ -> () - yield! data.TypeMapping |> List.map toString - match data.Remarks with - | Some x -> yield x |> toString - | None -> () - |] +let private xmlDocLines (xmlDoc: FSharpXmlDoc) = + match xmlDoc with + | FSharpXmlDoc.FromXmlText xmlDoc -> xmlDoc.GetElaboratedXmlLines() + | _ -> [||] + +let toToolTips (el: ToolTipElement) : Fable.Standalone.ToolTip[] = + let toToolTip (data: ToolTipElementData) : Fable.Standalone.ToolTip = + { + ParamName = data.ParamName + Signature = toTaggedText data.MainDescription + XmlDoc = xmlDocLines data.XmlDoc + TypeMapping = data.TypeMapping |> List.map toTaggedText |> List.toArray + Remarks = data.Remarks |> Option.map toTaggedText |> Option.defaultValue [||] + } match el with | ToolTipElement.None -> [||] - | ToolTipElement.Group(els) -> Seq.map dataToString els |> Array.concat - | ToolTipElement.CompositionError err -> [| err |] + | ToolTipElement.Group(els) -> els |> List.map toToolTip |> List.toArray + | ToolTipElement.CompositionError err -> + [| + { + ParamName = None + Signature = + [| + { + Tag = TextTag.Text + Text = err + } + |] + XmlDoc = [||] + TypeMapping = [||] + Remarks = [||] + } + |] /// Get tool tip at the specified location let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = @@ -316,7 +373,7 @@ let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> match findLongIdents (col - 1, lineText) with - | None -> [| "Cannot find ident for tooltip" |] + | None -> [||] | Some(col, identIsland) -> let (ToolTipText els) = checkFile.GetToolTip( @@ -327,7 +384,7 @@ let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = FSharp.Compiler.Tokenization.FSharpTokenTag.IDENT ) - Seq.map tooltipToString els |> Array.concat + List.map toToolTips els |> Array.concat | None -> [||] let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: int) lineText = @@ -535,7 +592,7 @@ let init () = let res = results :?> ParseAndCheckResults getDeclarationLocation res line col lineText - member _.GetToolTipText(results: IParseAndCheckResults, line: int, col: int, lineText: string) = + member _.GetToolTips(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getToolTipAtLocation res line col lineText diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index c4c3ebd528..0e6a1e7bb4 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -72,7 +72,7 @@ type WorkerAnswer = | CompilationFinished of code: string * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilationsFinished of code: string[] * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilerCrashed of message: string - | FoundTooltip of id: Guid * lines: string[] + | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] | FoundDeclarationLocation of id: Guid (* line1, col1, line2, col2 *) * (int * int * int * int) option diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index ec5c7dced5..675c87dc6b 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -431,12 +431,12 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetTooltip(id, line, col, lineText) -> - let tooltipLines = + let tooltips = match FILE_NAME |> state.CurrentResults.TryFind with | None -> [||] - | Some res -> fable.Manager.GetToolTipText(res, int line, int col, lineText) + | Some res -> fable.Manager.GetToolTips(res, int line, int col, lineText) - FoundTooltip(id, tooltipLines) |> state.Worker.Post + FoundTooltip(id, tooltips) |> state.Worker.Post return! loop box state | Some fable, GetCompletions(id, line, col, lineText) -> @@ -462,12 +462,12 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetTooltipForFile(id, file, line, col, lineText) -> - let tooltipLines = + let tooltips = match file |> state.CurrentResults.TryFind with | None -> [||] - | Some res -> fable.Manager.GetToolTipText(res, int line, int col, lineText) + | Some res -> fable.Manager.GetToolTips(res, int line, int col, lineText) - FoundTooltip(id, tooltipLines) |> state.Worker.Post + FoundTooltip(id, tooltips) |> state.Worker.Post return! loop box state | Some fable, GetCompletionsForFile(id, file, line, col, lineText) -> diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index 97fdb5d269..eb78c4a1a3 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -24,6 +24,9 @@ The fixture is generated, so nothing is downloaded. ## What it checks +It hovers a library function and a documented user function, and checks that the tooltip signature +has no stray spaces and keeps the FCS tags and the XML doc. + Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 6d414a464f..be9fa158c0 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -167,6 +167,32 @@ if (!(collisionErrors ?? []).some((e) => !e.IsWarning && e.Message.includes("sha } console.log(" BEAM example hashing and state-key diagnostics checked") +// Tooltips: FCS tagged text already carries its own spaces and punctuation, so the joined text must +// not gain any, and the tags and the XML doc must come through separately. +const greetLine = `let greet (name: string) = sprintf "Hello %s" name` +const runLine = `let run () = greet "world"` +post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine].join("\n") }], []]) +await answer("ParsedCode") + +const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") + +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000001", "Program.fs", 3, 29, greetLine]) +const [, , sprintfTooltip] = await answer("FoundTooltip") +const sprintfSignature = signatureOf(sprintfTooltip) +if (!sprintfSignature.includes("Printf.StringFormat<")) failures.push(`tooltip has stray spaces: ${sprintfSignature}`) +console.log(` tooltip: ${sprintfSignature}`) + +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000002", "Program.fs", 4, 15, runLine]) +const [, , greetTooltip] = await answer("FoundTooltip") +const signature = greetTooltip[0]?.Signature ?? [] +const greetSignature = signatureOf(greetTooltip) +if (!greetSignature.startsWith("val greet:")) failures.push(`unexpected tooltip signature: ${greetSignature}`) +if (!signature.some((t) => t.Tag === "keyword") || !signature.some((t) => t.Tag === "punctuation")) { + failures.push(`tooltip lost its tags: ${JSON.stringify(signature.slice(0, 4))}`) +} +if (!greetTooltip[0]?.XmlDoc?.includes("")) failures.push(`tooltip lost the XML doc: ${JSON.stringify(greetTooltip[0]?.XmlDoc)}`) +console.log(` tooltip: ${greetSignature} (${signature.length} tagged parts, ${greetTooltip[0]?.XmlDoc?.length ?? 0} doc lines)`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) From 1643297d9297fef536c8d10091d5b0d1fad77af7 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 16 Oct 2026 00:22:26 +0200 Subject: [PATCH 180/322] feat(js/ts/python/beam): support Async.TryCancelled (#5083) --- .../Python/Fable2Python.Transforms.fs | 7 +- src/fable-library-beam/fable_async.erl | 17 ++ src/fable-library-py/fable_library/async_.py | 18 +++ src/fable-library-ts/Async.ts | 18 +++ tests/Beam/AsyncTests.fs | 152 +++++++++++++++++ tests/Js/Main/AsyncTests.fs | 144 +++++++++++++++++ tests/Python/TestAsync.fs | 153 ++++++++++++++++++ tests/Python/TestType.fs | 8 + 8 files changed, 516 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 180e9855e1..4820be930b 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -1718,7 +1718,12 @@ let transformSet (com: IPythonCompiler) ctx range fableExpr typ (value: Fable.Ex let transformBindingExprBody (com: IPythonCompiler) (ctx: Context) (var: Fable.Ident) (value: Fable.Expr) = match value with | Function(args, body) -> - let name = Some var.Name + // decision: mutable callbacks keep their Callable annotation instead of inheriting a def's parameter names + let name = + if var.IsMutable then + None + else + Some var.Name let args', body', returnType, typeParams = Annotation.transformFunctionWithAnnotations com ctx name args body diff --git a/src/fable-library-beam/fable_async.erl b/src/fable-library-beam/fable_async.erl index 96ca37d44b..848b9f6594 100644 --- a/src/fable-library-beam/fable_async.erl +++ b/src/fable-library-beam/fable_async.erl @@ -9,6 +9,7 @@ parallel/1, sequential/1, catch_async/1, + try_cancelled/2, ignore/1, from_continuations/1, start_as_task/1, @@ -43,6 +44,7 @@ -spec parallel(list() | reference()) -> async(reference()). -spec sequential(list()) -> async(list()). -spec catch_async(async(term())) -> async(term()). +-spec try_cancelled(async(term()), fun((term()) -> term())) -> async(term()). -spec ignore(async(term())) -> async(ok). -spec from_continuations(fun()) -> async(term()). -spec start_as_task(async(term())) -> term(). @@ -479,6 +481,21 @@ catch_async(Computation) -> end end. +%% TryCancelled: observe cancellation without changing its terminal outcome. +try_cancelled(Computation, Compensation) -> + fun(Ctx) -> + OnCancel = maps:get(on_cancel, Ctx), + Ctx2 = Ctx#{ + on_cancel => fun(Error) -> + %% decision: preserve the original cancellation even when compensation throws, matching .NET. + try Compensation(Error) catch _:_ -> ok end, + OnCancel(Error) + end + }, + %% invariant: install the handler without adding a cancellation checkpoint, including for nested handlers. + Computation(Ctx2) + end. + %% Ignore: discard result ignore(Computation) -> fable_async_builder:bind(Computation, fun(_) -> fable_async_builder:return(ok) end). diff --git a/src/fable-library-py/fable_library/async_.py b/src/fable-library-py/fable_library/async_.py index 7f08e65750..462b951e8e 100644 --- a/src/fable-library-py/fable_library/async_.py +++ b/src/fable-library-py/fable_library/async_.py @@ -215,6 +215,23 @@ def _arrow21(__unit: Unit = UNIT) -> Async[Array[T]]: return delay(delayed) +def try_cancelled[T](computation: Async[T], compensation: Callable[[OperationCanceledError], None]) -> Async[T]: + def cont(ctx: IAsyncContext[T]) -> None: + def on_cancel(error: OperationCanceledError) -> None: + # decision: preserves the original cancellation when compensation throws, matching .NET + try: + compensation(error) + except Exception: + pass + ctx.trampoline.run(lambda: ctx.on_cancel(error)) + + ctx_ = IAsyncContext.create(ctx.trampoline, ctx.cancel_token, ctx.on_success, ctx.on_error, on_cancel) + # invariant: installs the handler without adding a cancellation checkpoint, including for nested handlers + computation(ctx_) + + return cont + + def catch_async[T](work: Async[T]) -> Async[FSharpChoice_2[T, Exception]]: def cont(ctx: IAsyncContext[FSharpChoice_2[T, Exception]]) -> None: def on_success(x: T): @@ -528,4 +545,5 @@ def exception_handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) "start_child", "start_immediate", "start_with_continuations", + "try_cancelled", ] diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index 09eaabee44..75ea833b19 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -127,6 +127,24 @@ export function catchAsync(work: Async) { }); } +export function tryCancelled(computation: Async, compensation: (error: OperationCanceledException) => void): Async { + return (ctx) => { + // invariant: install the handler without adding a cancellation checkpoint, including for nested handlers + computation({ + ...ctx, + onCancel: (error) => { + // decision: preserve the original cancellation even when compensation throws, matching .NET + try { compensation(error); } catch { /* Cancellation takes precedence. */ } + if (ctx.trampoline.incrementAndCheck()) { + ctx.trampoline.hijack(() => ctx.onCancel(error)); + } else { + ctx.onCancel(error); + } + }, + }); + }; +} + export function fromContinuations(f: (conts: Continuations) => void) { return (ctx: IAsyncContext) => { let settled = false; diff --git a/tests/Beam/AsyncTests.fs b/tests/Beam/AsyncTests.fs index b318fb914e..364f65d769 100644 --- a/tests/Beam/AsyncTests.fs +++ b/tests/Beam/AsyncTests.fs @@ -5,8 +5,160 @@ open Fable.Tests.Util open Util.Testing open System.Threading +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + let private raiseCaughtCancellation (error: exn) : unit = raise error +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + [] let ``test RunSynchronously optional timeout and cancellation token`` () = let work = async { return 42 } diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 2ca07f8b86..77a9965250 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1,9 +1,35 @@ module Fable.Tests.Async open System +open System.Threading open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + module private FinalizerRegressions = open System open System.Threading @@ -169,6 +195,124 @@ let private fromGenericCallback (callback: ('T -> unit) -> (exn -> unit) -> (Ope let tests = testList "Async" [ + testCase "TryCancelled preserves success and ordinary errors" <| fun () -> + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + + testCase "TryCancelled preserves cancellation identity and supports reuse" <| fun () -> + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + + testCase "TryCancelled installs nested handlers before cancellation" <| fun () -> + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + + testCase "TryCancelled handles deferred cancellation" <| fun () -> + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + + testCase "TryCancelled preserves finalizer order when handlers throw" <| fun () -> + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + + testCase "TryCancelled propagates downstream errors once" <| fun () -> + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + + testCaseAsync "TryCancelled preserves bounded trampoline execution" <| fun () -> async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + testCase "TryFinally installs cleanup before pre-cancellation" <| FinalizerRegressions.preCancelled testCase "Pre-cancelled nested TryFinally only runs outer cleanup" <| FinalizerRegressions.nestedPreCancelled testCase "Cancelled acquisition disposes use and use bang resources" <| FinalizerRegressions.cancelledAcquisition diff --git a/tests/Python/TestAsync.fs b/tests/Python/TestAsync.fs index b2c1755ecd..6d8cd7002d 100644 --- a/tests/Python/TestAsync.fs +++ b/tests/Python/TestAsync.fs @@ -1,9 +1,35 @@ module Fable.Tests.Async open System +open System.Threading open Util open Util.Testing +let private runOffThreadPool action = +#if FABLE_COMPILER + action () +#else + // .NET reschedules deferred FromContinuations callbacks invoked on a pool thread. + // Use a dedicated thread so the probe can observe completion and thrown callbacks. + let mutable failure = None + let thread = Thread(ThreadStart(fun () -> + try action () + with error -> failure <- Some error)) + thread.Start() + thread.Join() + failure |> Option.iter raise +#endif + +let private observeTryCancelled token (work: Async) = + let calls = ResizeArray() + Async.StartWithContinuations( + work, + (fun value -> calls.Add(string value)), + (fun error -> calls.Add("error: " + error.Message)), + (fun error -> calls.Add("cancel: " + error.Message)), + token) + calls + module private FinalizerRegressions = open System open System.Threading @@ -164,6 +190,133 @@ let errorWork: Async = Async.FromContinuations(fun (_,onError,_) -> onEr let cancelWork: Async = Async.FromContinuations(fun (_,_,onCancel) -> System.OperationCanceledException("cancelled") |> onCancel) +[] +let ``test TryCancelled preserves success and ordinary errors`` () = + let mutable compensations = 0 + let wrap work = Async.TryCancelled(work, fun _ -> compensations <- compensations + 1) + equal [|"42"|] ((observeTryCancelled CancellationToken.None (wrap (async.Return 42))).ToArray()) + for error in [Exception "body"; OperationCanceledException "thrown"] do + let work: Async = async { return raise error } + equal [|"error: " + error.Message|] ((observeTryCancelled CancellationToken.None (wrap work)).ToArray()) + let mutable completed = false + Async.StartWithContinuations( + Async.TryCancelled(async.Return (), fun _ -> compensations <- compensations + 1), + (fun () -> completed <- true), raise, raise) + equal true completed + equal 0 compensations + +[] +let ``test TryCancelled preserves cancellation identity and supports reuse`` () = + for throws in [false; true] do + let original = OperationCanceledException "original" + let calls = ResizeArray() + let mutable handlerSawOriginal = true + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel original) + let work = Async.TryCancelled(body, fun error -> + handlerSawOriginal <- handlerSawOriginal && obj.ReferenceEquals(original, error) + calls.Add "compensation" + if throws then failwith "compensation defect") + for _ in 1 .. 2 do + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), + (fun _ -> failwith "unexpected error"), + (fun error -> + equal true (obj.ReferenceEquals(original, error)) + calls.Add "cancel")) + equal [|"compensation"; "cancel"; "compensation"; "cancel"|] (calls.ToArray()) + equal true handlerSawOriginal + +[] +let ``test TryCancelled installs nested handlers before cancellation`` () = + use source = new CancellationTokenSource() + source.Cancel() + let mutable entered = false + let calls = ResizeArray() + let body = async { entered <- true; return 42 } + let inner = Async.TryCancelled(body, fun _ -> calls.Add "inner") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer") + Async.StartWithContinuations(outer, + (fun _ -> failwith "unexpected success"), raise, + (fun _ -> calls.Add "cancel"), source.Token) + equal false entered + equal [|"inner"; "outer"; "cancel"|] (calls.ToArray()) + +[] +let ``test TryCancelled handles deferred cancellation`` () = + runOffThreadPool (fun () -> + let original = OperationCanceledException "deferred" + let mutable cancel = fun (_: OperationCanceledException) -> failwith "not started" + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, continuation) -> cancel <- continuation) + let work = Async.TryCancelled(body, fun _ -> calls.Add "compensation") + Async.StartWithContinuations(work, + (fun _ -> failwith "unexpected success"), raise, + (fun error -> equal true (obj.ReferenceEquals(original, error)); calls.Add "cancel")) + equal 0 calls.Count + cancel original + equal [|"compensation"; "cancel"|] (calls.ToArray())) + +[] +let ``test TryCancelled preserves finalizer order when handlers throw`` () = + let calls = ResizeArray() + let body: Async = Async.FromContinuations(fun (_, _, cancel) -> cancel (OperationCanceledException "original")) + let finalized = async.TryFinally(body, fun () -> calls.Add "finally") + let inner = Async.TryCancelled(finalized, fun _ -> calls.Add "inner"; failwith "inner defect") + let outer = Async.TryCancelled(inner, fun _ -> calls.Add "outer"; failwith "outer defect") + equal [|"cancel: original"|] ((observeTryCancelled CancellationToken.None outer).ToArray()) + equal [|"finally"; "inner"; "outer"|] (calls.ToArray()) + +[] +let ``test TryCancelled propagates downstream errors once`` () = + runOffThreadPool (fun () -> + for channel in ["success"; "error"; "cancel"] do + let mutable compensations = 0 + let calls = ResizeArray() + let mutable complete = fun () -> failwith "not started" + let body: Async = Async.FromContinuations(fun (ok, error, cancel) -> + complete <- fun () -> + match channel with + | "success" -> ok 42 + | "error" -> error (Exception "body") + | _ -> cancel (OperationCanceledException "original")) + let work = Async.TryCancelled(body, fun _ -> compensations <- compensations + 1) + let terminal name = calls.Add name; failwith "downstream" + Async.StartWithContinuations(work, + (fun _ -> terminal "success"), + (fun _ -> terminal "error"), + (fun _ -> terminal "cancel")) + let caught = + try + complete () + "no exception" + with error -> error.Message + equal "downstream" caught + equal [|channel|] (calls.ToArray()) + equal (if channel = "cancel" then 1 else 0) compensations) + +[] +let ``test TryCancelled preserves bounded trampoline execution`` () = + async { + let original = OperationCanceledException "deep cancellation" + let mutable compensations = 0 + let rec loop remaining = + if remaining = 0 then + Async.FromContinuations(fun (_, _, cancel) -> cancel original) + else + Async.TryCancelled(async { + do! async.Return () + return! loop (remaining - 1) + }, fun _ -> compensations <- compensations + 1) + do! Async.FromContinuations(fun (ok, error, _) -> + Async.StartWithContinuations(loop 10000, + (fun () -> error (Exception "unexpected success")), error, + (fun cancelled -> + if obj.ReferenceEquals(original, cancelled) then ok () + else error (Exception "cancellation identity changed")))) + equal 10000 compensations + } + |> Async.RunSynchronously + [] let ``test Simple async translates without exception`` () = async { return () } diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index 29e5416dcb..fc8d31ad32 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -772,6 +772,14 @@ let ``test Optional named params work`` () = StaticClass.NamedParams(foo="14") |> equal 11 StaticClass.NamedParams() |> equal "John is 30 years old" +[] +let ``test Mutable function bindings accept replacement callbacks`` () = + let mutable callback = fun (initial: int) -> initial + 1 + let replace (replacement: int -> int) = callback <- replacement + callback 3 |> equal 4 + replace (fun differentName -> differentName * 2) + callback 3 |> equal 6 + [] let ``test F# optional param works`` () = let mutable f1 = fun (v: bool) -> From ec3967525132f8b4ffc65509547071673b98b34f Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 16 Oct 2026 00:33:09 +0200 Subject: [PATCH 181/322] chore(python): normalize runtime exports and enforce Ruff checks (#5090) --- .github/workflows/build.yml | 16 ++++ src/fable-library-py/fable_library/array_.py | 10 +-- .../fable_library/async_builder.py | 20 +++++ src/fable-library-py/fable_library/char.py | 1 - .../fable_library/core/__init__.py | 5 +- .../fable_library/diagnostics.py | 8 ++ src/fable-library-py/fable_library/event.py | 22 +++++ .../fable_library/exception_bases.py | 13 +++ src/fable-library-py/fable_library/file.py | 21 +++++ src/fable-library-py/fable_library/long.py | 25 ------ .../fable_library/map_util.py | 28 ++++++ .../fable_library/observable.py | 19 ++++ .../fable_library/protocols.py | 28 ++++++ .../fable_library/quotation.py | 69 +++++++++++++++ .../fable_library/reflection.py | 87 +++++++++++++++++++ .../fable_library/seq_native.py | 1 + src/fable-library-py/fable_library/thread.py | 5 ++ .../fable_library/time_span.py | 4 - src/fable-library-py/fable_library/uri.py | 6 ++ src/fable-library-py/fable_library/util.py | 4 +- src/fable-library-py/tests/test_ints.py | 1 - 21 files changed, 354 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e2920024f..3c65091a58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,22 @@ jobs: - name: Check F# code run: dotnet fantomas . --check --verbosity d + - name: Set up Python 3.13 + uses: actions/setup-python@v7 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Check Python imports and exports + run: uv run --frozen ruff check --select I,RUF022,RUF068 src/fable-library-py + + - name: Check Python formatting + run: uv run --frozen ruff format --check src/fable-library-py + # Verify that Fable.sln can be restored # See: https://github.com/fable-compiler/Fable/issues/3687 restore-project: diff --git a/src/fable-library-py/fable_library/array_.py b/src/fable-library-py/fable_library/array_.py index 72ce5665ef..2b8877c24d 100644 --- a/src/fable-library-py/fable_library/array_.py +++ b/src/fable-library-py/fable_library/array_.py @@ -196,6 +196,7 @@ "UInt64ArrayCons", "add_in_place", "add_range_in_place", + "all_pairs", "append", "average", "average_by", @@ -211,6 +212,7 @@ "create", "empty", "equals_with", + "exactly_one", "exists", "exists2", "exists_offset", @@ -261,8 +263,6 @@ "partition", "permute", "pick", - "reduce", - "reduce_back", "random_choice", "random_choice_by", "random_choice_with", @@ -278,6 +278,8 @@ "random_shuffle_in_place_by", "random_shuffle_in_place_with", "random_shuffle_with", + "reduce", + "reduce_back", "remove_all_in_place", "remove_at", "remove_in_place", @@ -301,8 +303,6 @@ "split_into", "sum", "sum_by", - "all_pairs", - "exactly_one", "tail", "take", "take_while", @@ -320,8 +320,8 @@ "unzip", "unzip3", "update_at", - "windowed", "where", + "windowed", "zero_create", "zip", "zip3", diff --git a/src/fable-library-py/fable_library/async_builder.py b/src/fable-library-py/fable_library/async_builder.py index e3f43ead41..c65ee3e558 100644 --- a/src/fable-library-py/fable_library/async_builder.py +++ b/src/fable-library-py/fable_library/async_builder.py @@ -456,3 +456,23 @@ def Zero(self) -> Async[None]: singleton = AsyncBuilder() + + +__all__ = [ + "AnonymousAsyncContext", + "Async", + "AsyncBuilder", + "CancellationCallbackError", + "CancellationToken", + "Continuations", + "IAsyncContext", + "OperationCanceledError", + "ScheduledItem", + "TaskCanceledError", + "Trampoline", + "empty_continuation", + "protected_bind", + "protected_cont", + "protected_return", + "singleton", +] diff --git a/src/fable-library-py/fable_library/char.py b/src/fable-library-py/fable_library/char.py index 9359054d4a..9be399678e 100644 --- a/src/fable-library-py/fable_library/char.py +++ b/src/fable-library-py/fable_library/char.py @@ -239,7 +239,6 @@ def parse(input: str) -> str: "is_number", "is_punctuation", "is_separator", - "is_separator", "is_surrogate", "is_surrogate_pair", "is_symbol", diff --git a/src/fable-library-py/fable_library/core/__init__.py b/src/fable-library-py/fable_library/core/__init__.py index 042ca692fa..66db388c1e 100644 --- a/src/fable-library-py/fable_library/core/__init__.py +++ b/src/fable-library-py/fable_library/core/__init__.py @@ -1,4 +1,7 @@ -from math import copysign as _copysign, fmod as _fmod, inf as _inf, nan as _nan +from math import copysign as _copysign +from math import fmod as _fmod +from math import inf as _inf +from math import nan as _nan from typing import Literal, SupportsFloat, SupportsInt from ._core import ( diff --git a/src/fable-library-py/fable_library/diagnostics.py b/src/fable-library-py/fable_library/diagnostics.py index 03c78829ca..fbe00a6a4e 100644 --- a/src/fable-library-py/fable_library/diagnostics.py +++ b/src/fable-library-py/fable_library/diagnostics.py @@ -54,3 +54,11 @@ def frequency() -> int: def get_timestamp() -> int64: return int64(time.perf_counter_ns()) + + +__all__ = [ + "StopWatch", + "TimerError", + "frequency", + "get_timestamp", +] diff --git a/src/fable-library-py/fable_library/event.py b/src/fable-library-py/fable_library/event.py index 115f2e7f7f..26aebcd35b 100644 --- a/src/fable-library-py/fable_library/event.py +++ b/src/fable-library-py/fable_library/event.py @@ -217,3 +217,25 @@ def create_event[T]( remove_handler: Callable[[DotNetDelegate[T]], None], ) -> IEvent[T]: return AnonymousEvent[T](add_handler, remove_handler) + + +__all__ = [ + "AnonymousEvent", + "Delegate", + "DotNetDelegate", + "Event", + "EventDelegate", + "IDelegateEvent", + "IEvent", + "IEvent_2", + "add", + "choose", + "create_event", + "filter", + "map", + "merge", + "pairwise", + "partition", + "scan", + "split", +] diff --git a/src/fable-library-py/fable_library/exception_bases.py b/src/fable-library-py/fable_library/exception_bases.py index 6e66ff5c20..817c257320 100644 --- a/src/fable-library-py/fable_library/exception_bases.py +++ b/src/fable-library-py/fable_library/exception_bases.py @@ -50,3 +50,16 @@ class MemoryErrorBase(ExceptionBase, MemoryError): class TimeoutErrorBase(ExceptionBase, TimeoutError): """A timeout error with .NET message and inner-exception support.""" + + +__all__ = [ + "ExceptionBase", + "IndexErrorBase", + "MemoryErrorBase", + "NotImplementedErrorBase", + "OverflowErrorBase", + "RuntimeErrorBase", + "TimeoutErrorBase", + "ValueErrorBase", + "ZeroDivisionErrorBase", +] diff --git a/src/fable-library-py/fable_library/file.py b/src/fable-library-py/fable_library/file.py index b2fb8b6ea3..dced0abab9 100644 --- a/src/fable-library-py/fable_library/file.py +++ b/src/fable-library-py/fable_library/file.py @@ -107,3 +107,24 @@ def replace(source_file_name: str, destination_file_name: str, destination_backu with open(destination_backup_file_name, "wb") as f: f.write(data) os.replace(source_file_name, destination_file_name) + + +__all__ = [ + "copy", + "delete", + "exists", + "move", + "read_all_bytes", + "read_all_bytes_async", + "read_all_lines", + "read_all_lines_async", + "read_all_text", + "read_all_text_async", + "replace", + "write_all_bytes", + "write_all_bytes_async", + "write_all_lines", + "write_all_lines_async", + "write_all_text", + "write_all_text_async", +] diff --git a/src/fable-library-py/fable_library/long.py b/src/fable-library-py/fable_library/long.py index 90febfa89f..0936936fbf 100644 --- a/src/fable-library-py/fable_library/long.py +++ b/src/fable-library-py/fable_library/long.py @@ -304,54 +304,29 @@ def to_string(x: int64 | uint64) -> str: "from_value", "get_range", "max", - "max", - "min", "min", "op_addition", - "op_addition", - "op_bitwise_and", "op_bitwise_and", "op_bitwise_or", - "op_bitwise_or", - "op_division", "op_division", "op_equality", - "op_equality", "op_exclusive_or", - "op_exclusive_or", - "op_greater_than", "op_greater_than", "op_greater_than_or_equal", - "op_greater_than_or_equal", "op_inequality", - "op_inequality", - "op_left_shift", "op_left_shift", "op_less_than", - "op_less_than", "op_less_than_or_equal", - "op_less_than_or_equal", - "op_logical_not", "op_logical_not", "op_modulus", - "op_modulus", "op_multiply", - "op_multiply", - "op_right_shift", "op_right_shift", "op_subtraction", - "op_subtraction", "op_unary_negation", - "op_unary_negation", - "op_unary_plus", "op_unary_plus", "parse", - "parse", - "sign", "sign", "to_number", "to_string", - "to_string", - "try_parse", "try_parse", ] diff --git a/src/fable-library-py/fable_library/map_util.py b/src/fable-library-py/fable_library/map_util.py index d33794bb5b..69ea4909a6 100644 --- a/src/fable-library-py/fable_library/map_util.py +++ b/src/fable-library-py/fable_library/map_util.py @@ -272,3 +272,31 @@ def assign(key: str, case_rule: CaseRules, value: Any): fail(kv_pair) return obj + + +__all__ = [ + "CaseRules", + "add_to_dict", + "add_to_set", + "change_case", + "contains_in_set", + "contains_value", + "copy_to_array_from_set", + "dashify", + "except_with_set", + "get_item_from_dict", + "intersect_with_set", + "is_proper_subset_of_set", + "is_proper_superset_of_set", + "is_subset_of_set", + "is_superset_of_set", + "key_value_list", + "make_dict", + "overlaps_with_set", + "remove_from_dict", + "remove_from_set", + "set_equals_set", + "symmetric_except_with_set", + "try_get_value", + "union_with_set", +] diff --git a/src/fable-library-py/fable_library/observable.py b/src/fable-library-py/fable_library/observable.py index 9c5cef70ae..7cb80d1fe1 100644 --- a/src/fable-library-py/fable_library/observable.py +++ b/src/fable-library-py/fable_library/observable.py @@ -216,3 +216,22 @@ def split[T, U, V]( choose(lambda v: Choice_tryValueIfChoice1Of2(splitter(v)), source), choose(lambda v: Choice_tryValueIfChoice2Of2(splitter(v)), source), ) + + +__all__ = [ + "IObservable", + "IObserver", + "Observable", + "Observer", + "add", + "choose", + "filter", + "map", + "merge", + "pairwise", + "partition", + "protect", + "scan", + "split", + "subscribe", +] diff --git a/src/fable-library-py/fable_library/protocols.py b/src/fable-library-py/fable_library/protocols.py index 49bd980945..a123a17f49 100644 --- a/src/fable-library-py/fable_library/protocols.py +++ b/src/fable-library-py/fable_library/protocols.py @@ -442,3 +442,31 @@ def keys(self, __unit: Unit = None) -> Iterable[K]: def __getitem__(self, key: K, /) -> V: """Get the value for a key.""" ... + + +__all__ = [ + "HashCode", + "ICollection", + "IComparable", + "IComparable_1", + "IComparer", + "IComparer_1", + "IDictionary_2", + "IDisposable", + "IEnumerable", + "IEnumerable_1", + "IEnumerator", + "IEqualityComparer", + "IEqualityComparer_1", + "IEquatable", + "IGenericAdder", + "IGenericAverager", + "IReadOnlyCollection_1", + "IReadOnlyDictionary", + "IReadOnlyDictionary_2", + "IReadOnlySet_1", + "ISet", + "IStructuralComparable", + "IStructuralEquatable", + "SupportsLessThan", +] diff --git a/src/fable-library-py/fable_library/quotation.py b/src/fable-library-py/fable_library/quotation.py index 8f2a821dd3..df41ab4a0a 100644 --- a/src/fable-library-py/fable_library/quotation.py +++ b/src/fable-library-py/fable_library/quotation.py @@ -698,3 +698,72 @@ def is_specific_call(template: Expr, expr: Expr) -> tuple[Expr | None, FSharpLis # names rather than System.Type, so it is always empty; callers match it with # a wildcard. return (instance, of_array(Array([])), of_array(expr.args)) + + +__all__ = [ + "Expr", + "ExprApplication", + "ExprCall", + "ExprFieldGet", + "ExprFieldSet", + "ExprIfThenElse", + "ExprLambda", + "ExprLet", + "ExprNewList", + "ExprNewRecord", + "ExprNewTuple", + "ExprNewUnion", + "ExprSequential", + "ExprTupleGet", + "ExprUnionField", + "ExprUnionTag", + "ExprValue", + "ExprVarExpr", + "ExprVarSet", + "Var", + "call_declaring_type", + "evaluate", + "expr_to_string", + "get_free_vars", + "get_type", + "is_and_also", + "is_application", + "is_call", + "is_field_get", + "is_if_then_else", + "is_lambda", + "is_let", + "is_new_record", + "is_new_tuple", + "is_new_union_case", + "is_or_else", + "is_sequential", + "is_specific_call", + "is_tuple_get", + "is_value", + "is_var", + "mk_application", + "mk_call", + "mk_field_get", + "mk_field_set", + "mk_if_then_else", + "mk_lambda", + "mk_let", + "mk_new_list", + "mk_new_record", + "mk_new_tuple", + "mk_new_union", + "mk_null", + "mk_quot_var", + "mk_sequential", + "mk_tuple_get", + "mk_union_field", + "mk_union_tag", + "mk_value", + "mk_var", + "mk_var_set", + "substitute", + "var_get_is_mutable", + "var_get_name", + "var_get_type", +] diff --git a/src/fable-library-py/fable_library/reflection.py b/src/fable-library-py/fable_library/reflection.py index 071b0636d8..b9e1fef101 100644 --- a/src/fable-library-py/fable_library/reflection.py +++ b/src/fable-library-py/fable_library/reflection.py @@ -500,3 +500,90 @@ def get_case_name(x: Any) -> str: def get_case_fields(x: Any) -> list[Any]: assert_union(x) return x.fields + + +__all__ = [ + "CaseInfo", + "Constructor", + "EnumCase", + "FieldInfo", + "MethodInfo", + "ParameterInfo", + "PropertyInfo", + "TypeInfo", + "anon_record_type", + "array_type", + "assert_union", + "bigint_type", + "bool_type", + "char_type", + "class_type", + "create_instance", + "decimal_type", + "delegate_type", + "enum_type", + "equals", + "float32_type", + "float64_type", + "full_name", + "get_case_fields", + "get_case_name", + "get_case_tag", + "get_element_type", + "get_enum_case", + "get_enum_name", + "get_enum_names", + "get_enum_underlying_type", + "get_enum_values", + "get_function_elements", + "get_generic_type_definition", + "get_generics", + "get_record_elements", + "get_record_field", + "get_record_fields", + "get_tuple_elements", + "get_tuple_field", + "get_tuple_fields", + "get_union_case_fields", + "get_union_cases", + "get_union_fields", + "get_value", + "int8_type", + "int16_type", + "int32_type", + "int64_type", + "is_array", + "is_enum", + "is_enum_defined", + "is_erased_to_number", + "is_function", + "is_generic_type", + "is_instance_of_type", + "is_record", + "is_subclass_of", + "is_tuple", + "is_union", + "lambda_type", + "list_type", + "make_generic_type", + "make_record", + "make_tuple", + "make_union", + "name", + "namespace", + "nativeint_type", + "obj_type", + "option_type", + "parse_enum", + "record_type", + "string_type", + "try_parse_enum", + "tuple_type", + "uint8_type", + "uint16_type", + "uint32_type", + "uint64_type", + "unativeint_type", + "union_type", + "unit_type", +] diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py index 1e2342442b..335ba69ebe 100644 --- a/src/fable-library-py/fable_library/seq_native.py +++ b/src/fable-library-py/fable_library/seq_native.py @@ -29,6 +29,7 @@ from .types import UNIT, Unit from .util import Enumerator, get_enumerator, to_iterator + _SENTINEL: Any = object() diff --git a/src/fable-library-py/fable_library/thread.py b/src/fable-library-py/fable_library/thread.py index b704c56ba4..4f0a17b78f 100644 --- a/src/fable-library-py/fable_library/thread.py +++ b/src/fable-library-py/fable_library/thread.py @@ -6,3 +6,8 @@ def sleep(milliseconds: int | TimeSpan) -> None: milliseconds_value = to_milliseconds(milliseconds) time.sleep(milliseconds_value / 1000.0) + + +__all__ = [ + "sleep", +] diff --git a/src/fable-library-py/fable_library/time_span.py b/src/fable-library-py/fable_library/time_span.py index 3467ce29ad..3e5de67fde 100644 --- a/src/fable-library-py/fable_library/time_span.py +++ b/src/fable-library-py/fable_library/time_span.py @@ -340,14 +340,10 @@ def to_milliseconds(value: int | TimeSpan) -> float: "subtract", "to_milliseconds", "total_days", - "total_days", - "total_hours", "total_hours", "total_microseconds", "total_milliseconds", "total_minutes", - "total_minutes", - "total_seconds", "total_seconds", "try_parse", ] diff --git a/src/fable-library-py/fable_library/uri.py b/src/fable-library-py/fable_library/uri.py index 1b71567eb5..1925728356 100644 --- a/src/fable-library-py/fable_library/uri.py +++ b/src/fable-library-py/fable_library/uri.py @@ -108,3 +108,9 @@ def try_create(uri: str | Uri, kind_or_uri: UriKind | Uri | str | int, out: FSha return True except Exception: return False + + +__all__ = [ + "Uri", + "UriKind", +] diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index a808660b3b..e4aaf03d4c 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -967,19 +967,19 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "Unit", "array_hash", "copy_to_array", + "create_random", "erase_unit_callable", "escape_data_string", "escape_uri_string", - "create_random", "get_platform", "identity_hash", "ignore", "nullable", "number_hash", "physical_hash", - "random_int", "random_bytes", "random_double", + "random_int", "range", "round", "to_iterable", diff --git a/src/fable-library-py/tests/test_ints.py b/src/fable-library-py/tests/test_ints.py index 828de2a5b0..cb30093192 100644 --- a/src/fable-library-py/tests/test_ints.py +++ b/src/fable-library-py/tests/test_ints.py @@ -2,7 +2,6 @@ from decimal import Decimal import pytest - from fable_library.core import FSharpRef, Int32, byte, float32, int16, int32, int64, sbyte, uint16, uint32, uint64 from fable_library.int32 import parse as int32_parse from fable_library.int32 import try_parse as int32_try_parse From 37c6980149a7d243fd74556933fdbc83b2610a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:30:23 -0700 Subject: [PATCH 182/322] fix(all): More union type tests (#5091) --- tests/Beam/UnionTypeTests.fs | 319 ++++++++++++----- tests/Dart/src/UnionTests.fs | 255 ++++++++++++- tests/Js/Main/UnionTypeTests.fs | 239 +++++++++++- tests/Php/TestUnionType.fs | 1 - tests/Python/TestUnionType.fs | 320 ++++++++++++++--- tests/Rust/tests/src/UnionTests.fs | 558 ++++++++++++++++------------- 6 files changed, 1303 insertions(+), 389 deletions(-) diff --git a/tests/Beam/UnionTypeTests.fs b/tests/Beam/UnionTypeTests.fs index a12f942397..0a88809eef 100644 --- a/tests/Beam/UnionTypeTests.fs +++ b/tests/Beam/UnionTypeTests.fs @@ -14,13 +14,13 @@ type Shape = | Square of side: float | Rectangle of width: float * height: float -type MyUnion = +type TestUnion = | Case0 | Case1 of string | Case2 of string * string | Case3 of string * string * string -type MyUnion2 = +type TestUnion2 = | Tag of string | NewTag of string @@ -33,20 +33,133 @@ type IntegrityLevel = | Untrusted -> "Untrusted" | Trusted -> "Trusted" -type IntUnion = - | IntCase1 of int - | IntCase2 of int - | IntCase3 of int +type T1 = T1 +type T2 = T2 +type T3 = T3 +type T4 = T4 +type T5 = T5 +type T6 = T6 +type T7 = T7 +type T8 = T8 +type T9 = T9 +type 'a more = More of 'a + +[] +type LangCode = + | EN + | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// [] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First [] type StructPoint2D = | Planar2D of float * float | Infinity +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + type WrappedUnion = | AString of string -type T1 = T1 +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" type ForwardedValue = ForwardedValue of obj @@ -73,7 +186,7 @@ let private unwrapGenericForwardedValue<'T> (message: obj) = Some value | _ -> None -let private isMyUnion (message: obj) = message :? MyUnion +let private isMyUnion (message: obj) = message :? TestUnion let private isEither (message: obj) = message :? Either let private isGender (message: obj) = message :? Gender let private isT1 (message: obj) = message :? T1 @@ -186,17 +299,6 @@ type CollectionOrderItem = type CollectionArray = CollectionArray of int array -type DeepRecord = { Value: string } - -type DeepWrappedUnion = - | DeepWrappedA of string * DeepRecord - | DeepWrappedB of string - | DeepWrappedC of int - | DeepWrappedD of DeepRecord - | DeepWrappedE of int * int - | DeepWrappedF of WrappedUnion - | DeepWrappedG of {| X: DeepRecord; Y: int |} - let (|Functional|NonFunctional|) (s: string) = match s with | "fsharp" | "haskell" | "ocaml" -> Functional @@ -314,14 +416,14 @@ let ``test collection extrema preserve first ties and project each item once`` ( obj.ReferenceEquals(arrayMinValue, first) |> equal true [] -type MyUnion3 = -| Case1 -| Case2 -| Case3 +type MyUnion = + | Case1 + | Case2 + | Case3 type R = { Name: string - UnionCase: MyUnion3 + Case: MyUnion } [] @@ -378,11 +480,11 @@ let ``test Union case with no data works`` () = let ``test Shape union works`` () = let area shape = match shape with - | Circle r -> 3.14159 * r * r - | Square s -> s * s - | Rectangle(w, h) -> w * h - area (Square 5.0) |> equal 25.0 - area (Rectangle(3.0, 4.0)) |> equal 12.0 + | Shape.Circle r -> 3.14159 * r * r + | Shape.Square s -> s * s + | Shape.Rectangle(w, h) -> w * h + area (Shape.Square 5.0) |> equal 25.0 + area (Shape.Rectangle(3.0, 4.0)) |> equal 12.0 [] let ``test Either union works`` () = @@ -432,12 +534,12 @@ let ``test Union cases matches with many arguments can be generated`` () = [] let ``test Pattern matching with common targets works`` () = - let x = MyUnion.Case2("a", "b") + let x = TestUnion.Case2("a", "b") match x with - | MyUnion.Case0 -> failwith "unexpected" - | MyUnion.Case1 _ - | MyUnion.Case2 _ -> "a" - | MyUnion.Case3(a, b, c) -> a + b + c + | TestUnion.Case0 -> failwith "unexpected" + | TestUnion.Case1 _ + | TestUnion.Case2 _ -> "a" + | TestUnion.Case3(a, b, c) -> a + b + c |> equal "a" [] @@ -488,13 +590,75 @@ let ``test Active patterns can be combined with union case matching`` () = [] let ``test Equality works in filter`` () = - let original = [| { Name = "1"; UnionCase = MyUnion3.Case1 } ; { Name = "2"; UnionCase = MyUnion3.Case1 }; { Name = "3"; UnionCase = MyUnion3.Case2 }; { Name = "4"; UnionCase = MyUnion3.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + original + |> Array.filter (fun r -> r.Case = MyUnion.Case1) + |> Array.length + |> equal 2 + +[] +let ``test Pattern matching works in filter`` () = + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original - |> Array.filter (fun r -> r.UnionCase = MyUnion3.Case1) + |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 -// --- Tests ported from Rust UnionTests --- +[] +let ``test Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + +[] +let ``test Verify that union cases can have `-` in their names`` () = + // See https://github.com/fable-compiler/Fable/issues/4071 + let x = LangCode.``pt-Br`` + equal x LangCode.``pt-Br`` + +[] +let ``test sprintf formats strings cases correctly`` () = + let s = sprintf "%A" (S "1") + s |> equal "S \"1\"" + +[] +let ``test Union cases with no fields are physically equal`` () = + // Nullary cases compile to plain atoms, which the BEAM VM interns, so this matches fsc. + obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true +#if FABLE_COMPILER + // Unlike fsc, Erlang has no reference identity for compound terms (only atoms are + // interned), so `=:=` here is structural equality, unlike .NET reference equality. + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal true +#else + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false +#endif + obj.ReferenceEquals(T1, T1) |> equal true + +[] +let ``Named union case fields work when a field is called name`` () = + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + +[] +let ``test Static union member returning the union type works`` () = + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + +[] +let ``test Static union member with nested self-reference works`` () = + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First [] let ``test Struct unions work`` () = @@ -506,6 +670,31 @@ let ``test Struct unions work`` () = let res = distance p res |> equal 5. +[] +let ``test Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape.Area circle |> equal (System.Math.PI * radius * radius) + StructShape.Area square |> equal (4.0 * radius * radius) + +[] +let ``test Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape2.Area circle |> equal (System.Math.PI * radius * radius) + StructShape2.Area square |> equal (4.0 * radius * radius) + +[] +let ``test Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + [] let ``test Union case matching works`` () = let x = IntCase1 5 @@ -524,15 +713,6 @@ let ``test Union case equality works`` () = IntCase2 1 = IntCase2 1 |> equal true IntCase3 1 = IntCase3 1 |> equal true -let unionFnAlways1 = function - | IntCase1 x -> x - | _ -> -1 - -let unionFnRetNum = function - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - [] let ``test Union fn call works`` () = let x = IntCase1 3 @@ -551,24 +731,6 @@ let ``test Union with wrapped type works`` () = let b = match a with AString s -> s + " world" b |> equal "hello world" -let matchStrings = function - | DeepWrappedA (s, d) -> d.Value + s - | DeepWrappedB s -> s - | DeepWrappedC _ -> "nothing" - | DeepWrappedD d -> d.Value - | DeepWrappedE _ -> "nothing2" - | DeepWrappedF (AString s) -> s - | DeepWrappedG x -> x.X.Value - -let matchNumbers = function - | DeepWrappedA _ -> 0 - | DeepWrappedB _ -> 0 - | DeepWrappedC c -> c - | DeepWrappedD _ -> 0 - | DeepWrappedE(a, b) -> a + b - | DeepWrappedF _ -> 0 - | DeepWrappedG x -> x.Y - [] let ``test Deep union with wrapped type works`` () = let a = DeepWrappedA (" world", { Value = "hello" }) @@ -587,34 +749,21 @@ let ``test Deep union with wrapped type works`` () = [] let ``test Deep union with tuped prim type works`` () = let e = DeepWrappedE (3, 2) - let c = DeepWrappedC 42 + let c = DeepWrappedC 42 //todo to string! let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} e |> matchNumbers |> equal 5 c |> matchNumbers |> equal 42 g |> matchNumbers |> equal 365 -let matchStringWhenNotHello = function - | DeepWrappedB s when s <> "hello" -> "not hello" - | _ -> "hello" +[] +let ``test Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" [] let ``test Match with condition works`` () = let b1 = DeepWrappedB "hello" let b2 = DeepWrappedB "not" - b1 |> matchStringWhenNotHello |> equal "hello" - b2 |> matchStringWhenNotHello |> equal "not hello" - -[] -let ``test Union cases with no fields are physically equal`` () = - // Nullary cases compile to plain atoms, which the BEAM VM interns, so this matches fsc. - obj.ReferenceEquals(Male, Male) |> equal true - obj.ReferenceEquals(Male, Female) |> equal false - obj.ReferenceEquals(Case0, Case0) |> equal true - obj.ReferenceEquals(T1, T1) |> equal true -#if FABLE_COMPILER - // Unlike fsc, Erlang has no reference identity for compound terms (only atoms are - // interned), so `=:=` here is structural equality, unlike .NET reference equality. - obj.ReferenceEquals(Case1 "a", Case1 "a") |> equal true -#else - obj.ReferenceEquals(Case1 "a", Case1 "a") |> equal false -#endif + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Dart/src/UnionTests.fs b/tests/Dart/src/UnionTests.fs index 11125bea46..55d70fefe9 100644 --- a/tests/Dart/src/UnionTests.fs +++ b/tests/Dart/src/UnionTests.fs @@ -18,7 +18,6 @@ type TestUnion = | Case2 of string * string | Case3 of string * string * string - type TestUnion2 = | Tag of string | NewTag of string @@ -64,9 +63,9 @@ type Wrapper(s: string) = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -89,10 +88,123 @@ type T8 = T8 type T9 = T9 type 'a more = More of 'a +[] +type LangCode = + | EN + | ``pt-Br`` + +type S = S of string + type Id = Id of string type UnionWithIdField = WithId of Id: Id +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// [] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + // member this.Area = // TODO: support instance members + static member Area this = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + let tests() = testCase "Union cases matches with no arguments can be generated" <| fun () -> let x = Male @@ -191,14 +303,14 @@ let tests() = ex.Message.Contains("bar") |> equal false testCase "Equality works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length |> equal 2 testCase "Pattern matching works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length @@ -218,3 +330,134 @@ let tests() = // | Str s -> "s: " + s // Str "foo" |> strify |> equal "s: foo" // Int 42 |> strify |> equal "i: 42" + + testCase "Verify that union cases can have `-` in their names" <| fun () -> + // See https://github.com/fable-compiler/Fable/issues/4071 + let x = LangCode.``pt-Br`` + equal x LangCode.``pt-Br`` + + // testCase "sprintf formats strings cases correctly" <| fun () -> + // let s = sprintf "%A" (S "1") + // equal s "S \"1\"" + + // testCase "Union cases with no fields are physically equal" <| fun () -> + // obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + // obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + // obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true + // obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false + // obj.ReferenceEquals(T1, T1) |> equal true + + testCase "Named union case fields work when a field is called name" <| fun () -> + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + + testCase "Static union member returning the union type works" <| fun () -> + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + + testCase "Static union member with nested self-reference works" <| fun () -> + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + + testCase "Struct unions work" <| fun () -> + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> System.Double.PositiveInfinity //TODO: infinity literal + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + + testCase "Struct unions with fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape.Area circle |> equal (System.Math.PI * radius * radius) + StructShape.Area square |> equal (4.0 * radius * radius) + + testCase "Struct unions without fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + // circle.Area |> equal (System.Math.PI * radius * radius) + // square.Area |> equal (4.0 * radius * radius) + StructShape2.Area circle |> equal (System.Math.PI * radius * radius) + StructShape2.Area square |> equal (4.0 * radius * radius) + + testCase "Nested union match keeps case-specific field access" <| fun () -> + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + + testCase "Union case matching works" <| fun () -> + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + + testCase "Union case equality works" <| fun () -> + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + + testCase "Union fn call works" <| fun () -> + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + + testCase "Union with wrapped type works" <| fun () -> + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + + testCase "Deep union with wrapped type works" <| fun () -> + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + + testCase "Deep union with tuped prim type works" <| fun () -> + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + + testCase "Multi-case Union with wrapped type works" <| fun () -> + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + + testCase "Match with condition works" <| fun () -> + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Js/Main/UnionTypeTests.fs b/tests/Js/Main/UnionTypeTests.fs index 002d57dcda..db48e9698c 100644 --- a/tests/Js/Main/UnionTypeTests.fs +++ b/tests/Js/Main/UnionTypeTests.fs @@ -18,7 +18,6 @@ type TestUnion = | Case2 of string * string | Case3 of string * string * string - type TestUnion2 = | Tag of string | NewTag of string @@ -64,9 +63,9 @@ type Wrapper(s: string) = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -106,6 +105,114 @@ type LangCode = type S = S of string +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +[] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + let tests = testList "Unions" [ testCase "Union cases matches with no arguments can be generated" <| fun () -> @@ -229,19 +336,26 @@ let tests = |> equal 2 testCase "Equality works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length |> equal 2 testCase "Pattern matching works in filter" <| fun () -> - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 + testCase "Union field can have the same name as its type" <| fun () -> + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + #if FABLE_COMPILER testCase "Case testing with generic erased unions works" <| fun () -> let strify (x: U2) = @@ -299,7 +413,6 @@ let tests = testCase "Verify that union cases can have `-` in their names" <| fun () -> // See https://github.com/fable-compiler/Fable/issues/4071 let x = LangCode.``pt-Br`` - equal x LangCode.``pt-Br`` testCase "sprintf formats strings cases correctly" <| fun () -> @@ -312,4 +425,116 @@ let tests = obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false obj.ReferenceEquals(T1, T1) |> equal true + + testCase "Named union case fields work when a field is called name" <| fun () -> + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + + testCase "Static union member returning the union type works" <| fun () -> + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + + testCase "Static union member with nested self-reference works" <| fun () -> + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + + testCase "Struct unions work" <| fun () -> + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + + testCase "Struct unions with fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + + testCase "Struct unions without fields work" <| fun () -> + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + + testCase "Nested union match keeps case-specific field access" <| fun () -> + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + + testCase "Union case matching works" <| fun () -> + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + + // testCase "Union case equality works" <| fun () -> + // IntCase1 5 = IntCase1 5 |> equal true + // IntCase1 5 = IntCase2 5 |> equal false + // IntCase3 2 = IntCase3 3 |> equal false + // IntCase2 1 = IntCase2 1 |> equal true + // IntCase3 1 = IntCase3 1 |> equal true + + testCase "Union fn call works" <| fun () -> + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + + testCase "Union with wrapped type works" <| fun () -> + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + + testCase "Deep union with wrapped type works" <| fun () -> + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + + testCase "Deep union with tuped prim type works" <| fun () -> + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + + testCase "Multi-case Union with wrapped type works" <| fun () -> + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + + testCase "Match with condition works" <| fun () -> + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" + ] diff --git a/tests/Php/TestUnionType.fs b/tests/Php/TestUnionType.fs index 6d462a81de..e2e66ba784 100644 --- a/tests/Php/TestUnionType.fs +++ b/tests/Php/TestUnionType.fs @@ -18,7 +18,6 @@ type Either<'TL,'TR> = // | Case2 of string * string // | Case3 of string * string * string - // type TestUnion2 = // | Tag of string // | NewTag of string diff --git a/tests/Python/TestUnionType.fs b/tests/Python/TestUnionType.fs index 385e098bf2..bda4c2a22c 100644 --- a/tests/Python/TestUnionType.fs +++ b/tests/Python/TestUnionType.fs @@ -4,8 +4,6 @@ open Util.Testing type Gender = Male | Female -type T1 = T1 - type Either<'TL,'TR> = | Left of 'TL | Right of 'TR @@ -14,14 +12,13 @@ type Either<'TL,'TR> = | Left y -> y.ToString() | Right y -> y.ToString() -type MyUnion = +type TestUnion = | Case0 | Case1 of string | Case2 of string * string | Case3 of string * string * string - -type MyUnion2 = +type TestUnion2 = | Tag of string | NewTag of string @@ -65,16 +62,151 @@ type Wrapper(s: string) = member x.Value = s |> Seq.rev |> Seq.map string |> String.concat "" [] -type MyUnion3 = +type MyUnion = | Case1 | Case2 | Case3 type R = { Name: string - Case: MyUnion3 + Case: MyUnion } +type T1 = T1 +type T2 = T2 +type T3 = T3 +type T4 = T4 +type T5 = T5 +type T6 = T6 +type T7 = T7 +type T8 = T8 +type T9 = T9 +type 'a more = More of 'a + +// [] +// type LangCode = +// | EN +// | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id + +// See https://github.com/fable-compiler/Fable/issues/4645 +// A named union case field called `name` collided with the inherited `Union.name` +// property, which Python's @dataclass treated as a default value. +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +// See https://github.com/fable-compiler/Fable/issues/4736 +// A static member whose type references the enclosing union used to emit a class-body +// `StaticLazyProperty[...]` subscript that evaluated the union type before its name was bound, +// raising `NameError` at import time. The reference can be nested in any type shape, so each +// member below exercises a different one: array, option (rendered with `|`), nested list/array, +// tuple, and a function type (`unit -> Example`). +[] +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + #if FABLE_COMPILER open Fable.Core @@ -132,12 +264,12 @@ let ``test Union cases matches with many arguments can be generated`` () = [] let ``test Pattern matching with common targets works`` () = - let x = MyUnion.Case2("a", "b") + let x = TestUnion.Case2("a", "b") match x with - | MyUnion.Case0 -> failwith "unexpected" - | MyUnion.Case1 _ - | MyUnion.Case2 _ -> "a" - | MyUnion.Case3(a, b, c) -> a + b + c + | TestUnion.Case0 -> failwith "unexpected" + | TestUnion.Case1 _ + | TestUnion.Case2 _ -> "a" + | TestUnion.Case3(a, b, c) -> a + b + c |> equal "a" [] @@ -231,23 +363,46 @@ let ``test Erased union payload fields can be read`` () = [] let ``test Equality works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion3.Case1 } ; { Name = "2"; Case = MyUnion3.Case1 }; { Name = "3"; Case = MyUnion3.Case2 }; { Name = "4"; Case = MyUnion3.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original - |> Array.filter (fun r -> r.Case = MyUnion3.Case1) + |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length |> equal 2 -type S = S of string +[] +let ``test Pattern matching works in filter`` () = + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + original + |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) + |> Array.length + |> equal 2 + +[] +let ``test Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + +// [] +// let ``test Verify that union cases can have `-` in their names`` () = +// // See https://github.com/fable-compiler/Fable/issues/4071 +// let x = LangCode.``pt-Br`` +// equal x LangCode.``pt-Br`` + [] let ``test sprintf formats strings cases correctly`` () = let s = sprintf "%A" (S "1") s |> equal "S \"1\"" -// See https://github.com/fable-compiler/Fable/issues/4645 -// A named union case field called `name` collided with the inherited `Union.name` -// property, which Python's @dataclass treated as a default value. -type NamedFieldUnion = - | NamedCase of name: string * optional: string +[] +let ``test Union cases with no fields are physically equal`` () = + obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true + obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false + obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true + obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false + obj.ReferenceEquals(T1, T1) |> equal true [] let ``test Named union case fields work when a field is called name`` () = @@ -257,33 +412,6 @@ let ``test Named union case fields work when a field is called name`` () = name |> equal "v1" optional |> equal "v2" -[] -let ``test Union cases with no fields are physically equal`` () = - obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true - obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false - obj.ReferenceEquals(MyUnion.Case0, MyUnion.Case0) |> equal true - obj.ReferenceEquals(MyUnion.Case1 "a", MyUnion.Case1 "a") |> equal false - obj.ReferenceEquals(T1, T1) |> equal true - -// See https://github.com/fable-compiler/Fable/issues/4736 -// A static member whose type references the enclosing union used to emit a class-body -// `StaticLazyProperty[...]` subscript that evaluated the union type before its name was bound, -// raising `NameError` at import time. The reference can be nested in any type shape, so each -// member below exercises a different one: array, option (rendered with `|`), nested list/array, -// tuple, and a function type (`unit -> Example`). -[] -[] -type Example = - | First - | Second - | Other of string - - static member All = [| Example.First; Example.Second |] - static member Head = Some Example.First - static member Nested = [ [| Example.First |] ] - static member Pair = (Example.First, Example.Second) - static member Factory: unit -> Example = fun () -> Example.First - [] let ``test Static union member returning the union type works`` () = Example.All |> Array.length |> equal 2 @@ -301,3 +429,101 @@ let ``test Static union member with nested self-reference works`` () = Example.Pair |> snd |> equal Example.Second // Function type (`Callable[[], _Example]`) — missed by the original narrower detection. Example.Factory() |> equal Example.First + +[] +let ``test Struct unions work`` () = + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + +[] +let ``test Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``test Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``test Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + +[] +let ``test Union case matching works`` () = + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + +[] +let ``test Union case equality works`` () = + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + +[] +let ``test Union fn call works`` () = + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + +[] +let ``test Deep union with wrapped type works`` () = + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + +[] +let ``test Deep union with tuped prim type works`` () = + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + +[] +let ``test Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + +[] +let ``test Match with condition works`` () = + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" diff --git a/tests/Rust/tests/src/UnionTests.fs b/tests/Rust/tests/src/UnionTests.fs index 1572ddf3f2..3b8378a02d 100644 --- a/tests/Rust/tests/src/UnionTests.fs +++ b/tests/Rust/tests/src/UnionTests.fs @@ -2,244 +2,7 @@ module Fable.Tests.UnionTests open Util.Testing -type IntUnion = - | IntCase1 of int - | IntCase2 of int - | IntCase3 of int - -[] -type Point2D = - | Planar2D of float * float - | Infinity - -[] -type Shape = - | Square of side: int - | Circle of radius: float - - member this.Area = - match this with - | Square side -> float (side * side) - | Circle radius -> System.Math.PI * radius * radius - -[] -type Shape2 = - | Square of float - | Circle of float - - member this.Area = - match this with - | Square side -> side * side - | Circle radius -> System.Math.PI * radius * radius - -[] -let ``Struct unions work`` () = - let distance p = - match p with - | Planar2D (x, y) -> sqrt (x * x + y * y) - | Infinity -> infinity - let p = Planar2D (3, 4) - let res = distance p - res |> equal 5. - -[] -let ``Struct unions with fields work`` () = - let radius = 3.0 - let circle = Shape.Circle radius - let square = Shape.Square(int (2.0 * radius)) - circle.Area |> equal (System.Math.PI * radius * radius) - square.Area |> equal (4.0 * radius * radius) - -[] -let ``Struct unions without fields work`` () = - let radius = 3.0 - let circle = Shape2.Circle radius - let square = Shape2.Square(2.0 * radius) - circle.Area |> equal (System.Math.PI * radius * radius) - square.Area |> equal (4.0 * radius * radius) - -[] -let ``Union case matching works`` () = - let x = IntCase1 5 - let res = - match x with - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - res |> equal 5 - -[] -let ``Union case equality works`` () = - IntCase1 5 = IntCase1 5 |> equal true - IntCase1 5 = IntCase2 5 |> equal false - IntCase3 2 = IntCase3 3 |> equal false - IntCase2 1 = IntCase2 1 |> equal true - IntCase3 1 = IntCase3 1 |> equal true - -let unionFnAlways1 = function - | IntCase1 x -> x - | _ -> -1 - -let unionFnRetNum = function - | IntCase1 a -> a - | IntCase2 b -> b - | IntCase3 c -> c - -[] -let ``Union fn call works`` () = - let x = IntCase1 3 - let res = unionFnAlways1 x - let res2 = unionFnAlways1 x //deliberately force clone/borrow break - let res3 = unionFnRetNum x - let res4 = unionFnRetNum (IntCase2 24) - res |> equal 3 - res2 |> equal 3 - res3 |> equal 3 - res4 |> equal 24 - -type WrappedUnion = - | AString of string - -[] -let ``Union with wrapped type works`` () = - let a = AString "hello" - let b = match a with AString s -> s + " world" - b |> equal "hello world" - -type DeepRecord = {Value: string} - -type DeepWrappedUnion = - | DeepWrappedA of string * DeepRecord - | DeepWrappedB of string - | DeepWrappedC of int - | DeepWrappedD of DeepRecord - | DeepWrappedE of int * int - | DeepWrappedF of WrappedUnion - | DeepWrappedG of {| X: DeepRecord; Y: int|} - -let matchStrings = function - | DeepWrappedA (s, d) -> d.Value + s - | DeepWrappedB s -> s - | DeepWrappedC c -> "nothing" - | DeepWrappedD d -> d.Value - | DeepWrappedE(a, b) -> "nothing2" - | DeepWrappedF (AString s) -> s - | DeepWrappedG x -> x.X.Value - -let matchNumbers = function - | DeepWrappedA (s, d) -> 0 - | DeepWrappedB s -> 0 - | DeepWrappedC c -> c - | DeepWrappedD d -> 0 - | DeepWrappedE(a, b) -> a + b - | DeepWrappedF _ -> 0 - | DeepWrappedG x -> x.Y - -[] -let ``Deep union with wrapped type works`` () = - let a = DeepWrappedA (" world", {Value = "hello"}) - let b = DeepWrappedB "world" - let c = DeepWrappedC 42 - let d = DeepWrappedD { Value = "hello" } - let f = DeepWrappedF (AString "doublewrapped") - let g = DeepWrappedG {|X={Value="G"}; Y=365|} - a |> matchStrings |> equal "hello world" - b |> matchStrings |> equal "world" - c |> matchStrings |> equal "nothing" - d |> matchStrings |> equal "hello" - f |> matchStrings |> equal "doublewrapped" - g |> matchStrings |> equal "G" - -[] -let ``Deep union with tuped prim type works`` () = - let e = DeepWrappedE (3, 2) - let c = DeepWrappedC 42//todo to string! - let g = DeepWrappedG {|X={Value="G"}; Y=365|} - e |> matchNumbers |> equal 5 - c |> matchNumbers |> equal 42 - g |> matchNumbers |> equal 365 - -[] -let ``Multi-case Union with wrapped type works`` () = - let b = DeepWrappedB "hello" - let res = match b with DeepWrappedB s -> s + " world" | _ -> "" - res |> equal "hello world" - -[] -let ``Should correctly import union in other file and allow creation`` () = - let r = Common.Imports.MyUnion.A 1 - let expected = Common.Imports.MyUnion.createA 1 - r |> equal expected - - -let matchStringWhenStringNotHello = function - | DeepWrappedB s when s <> "hello" -> "not hello" - | _ -> "hello" - -[] -let ``Match with condition works`` () = - let b1 = DeepWrappedB "hello" - let b2 = DeepWrappedB "not" - b1 |> matchStringWhenStringNotHello |> equal "hello" - b2 |> matchStringWhenStringNotHello |> equal "not hello" - -[] -type NestedMatchUnion = - | NestedA of int - | NestedB of int - -let private nestedUnionCaseAccess (u: NestedMatchUnion) = - match u with - | NestedA _ -> 0 - | NestedB _ -> - match u with - | NestedA value -> value - | _ -> -1 - -[] -let ``Nested union match keeps case-specific field access`` () = - nestedUnionCaseAccess (NestedA 7) |> equal 0 - nestedUnionCaseAccess (NestedB 11) |> equal -1 - -[] -type ArcUnion = - | ArcS of string - | ArcI of int - -let add1 = function - | ArcS s -> s + "1" |> ArcS - | ArcI i -> i + 1 |> ArcI - -[] -let ``Union with pointer type works`` () = - let a = ArcS "hello" - let b = ArcI 42 - let aa = add1 a - let bb = add1 b - aa |> equal (ArcS "hello1") - bb |> equal (ArcI 43) - -#if FABLE_COMPILER -open Fable.Core - -[")>] -let ensureMyUnionWrapped s = nativeOnly - -[] -let ensureIsStructUnionUnwrapped s = nativeOnly - -[] -let ``Normal union should be wrapped in a Lrc`` () = - IntCase1 1 |> ensureMyUnionWrapped |> ignore - -[] -let ``Struct union should not be wrapped in a Lrc`` () = - Infinity |> ensureIsStructUnionUnwrapped |> ignore - -#endif - - -// type Gender = Male | Female +type Gender = Male | Female // type Either<'TL,'TR> = // | Left of 'TL @@ -300,9 +63,9 @@ type Tree = [] type MyUnion = -| Case1 -| Case2 -| Case3 + | Case1 + | Case2 + | Case3 type R = { Name: string @@ -328,6 +91,119 @@ type T8 = T8 type T9 = T9 type 'a more = More of 'a +// [] +// type LangCode = +// | EN +// | ``pt-Br`` + +type S = S of string + +type Id = Id of string + +type UnionWithIdField = WithId of Id: Id +type NamedFieldUnion = + | NamedCase of name: string * optional: string + +[] +type Example = + | First + | Second + | Other of string + + static member All = [| Example.First; Example.Second |] + static member Head = Some Example.First + static member Nested = [ [| Example.First |] ] + static member Pair = (Example.First, Example.Second) + static member Factory: unit -> Example = fun () -> Example.First + +[] +type StructPoint2D = + | Planar2D of float * float + | Infinity + +[] +type StructShape = + | Square of side: int + | Circle of radius: float + + member this.Area = + match this with + | Square side -> float (side * side) + | Circle radius -> System.Math.PI * radius * radius + +[] +type StructShape2 = + | Square of float + | Circle of float + + member this.Area = + match this with + | Square side -> side * side + | Circle radius -> System.Math.PI * radius * radius + +[] +type NestedMatchUnion = + | NestedA of int + | NestedB of int + +let private nestedUnionCaseAccess (u: NestedMatchUnion) = + match u with + | NestedA _ -> 0 + | NestedB _ -> + match u with + | NestedA value -> value + | _ -> -1 + +type IntUnion = + | IntCase1 of int + | IntCase2 of int + | IntCase3 of int + +let unionFnAlways1 = function + | IntCase1 x -> x + | _ -> -1 + +let unionFnRetNum = function + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + +type WrappedUnion = + | AString of string + +type DeepRecord = { Value: string } + +type DeepWrappedUnion = + | DeepWrappedA of string * DeepRecord + | DeepWrappedB of string + | DeepWrappedC of int + | DeepWrappedD of DeepRecord + | DeepWrappedE of int * int + | DeepWrappedF of WrappedUnion + | DeepWrappedG of {| X: DeepRecord; Y: int|} + +let matchStrings = function + | DeepWrappedA (s, d) -> d.Value + s + | DeepWrappedB s -> s + | DeepWrappedC c -> "nothing" + | DeepWrappedD d -> d.Value + | DeepWrappedE(a, b) -> "nothing2" + | DeepWrappedF (AString s) -> s + | DeepWrappedG x -> x.X.Value + +let matchNumbers = function + | DeepWrappedA (s, d) -> 0 + | DeepWrappedB s -> 0 + | DeepWrappedC c -> c + | DeepWrappedD d -> 0 + | DeepWrappedE(a, b) -> a + b + | DeepWrappedF _ -> 0 + | DeepWrappedG x -> x.Y + +let matchStringWhenStringNotHello = function + | DeepWrappedB s when s <> "hello" -> "not hello" + | _ -> "hello" + // [] // let ``Union cases matches with no arguments can be generated`` () = // let x = Male @@ -436,7 +312,7 @@ let ``Union cases called Tag still work (bug due to Tag field)`` () = [] let ``Equality works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> r.Case = MyUnion.Case1) |> Array.length @@ -444,12 +320,20 @@ let ``Equality works in filter`` () = [] let ``Pattern matching works in filter`` () = - let original = [| { Name = "1"; Case = MyUnion.Case1 } ; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] + let original = [| { Name = "1"; Case = MyUnion.Case1 }; { Name = "2"; Case = MyUnion.Case1 }; { Name = "3"; Case = MyUnion.Case2 }; { Name = "4"; Case = MyUnion.Case3 } |] original |> Array.filter (fun r -> match r.Case with MyUnion.Case1 -> true | _ -> false) |> Array.length |> equal 2 +[] +let ``Union field can have the same name as its type`` () = + let value = WithId(Id "foo") + match value with + | WithId id -> + match id with + | Id value -> value |> equal "foo" + // #if FABLE_COMPILER // [] // let ``Erased union type testing works`` () = @@ -520,3 +404,191 @@ let ``Implicit conversion to erased unions works`` () = b |> equal (U3.Case3 true) #endif + +// [] +// let ``Verify that union cases can have `-` in their names`` () = +// // See https://github.com/fable-compiler/Fable/issues/4071 +// let x = LangCode.``pt-Br`` +// equal x LangCode.``pt-Br`` + +// [] +// let ``sprintf formats strings cases correctly`` () = +// let s = sprintf "%A" (S "1") +// s |> equal "S \"1\"" + +// [] +// let ``Union cases with no fields are physically equal`` () = +// obj.ReferenceEquals(Gender.Male, Gender.Male) |> equal true +// obj.ReferenceEquals(Gender.Male, Gender.Female) |> equal false +// obj.ReferenceEquals(TestUnion.Case0, TestUnion.Case0) |> equal true +// obj.ReferenceEquals(TestUnion.Case1 "a", TestUnion.Case1 "a") |> equal false +// obj.ReferenceEquals(T1, T1) |> equal true + +[] +let ``Named union case fields work when a field is called name`` () = + let value = NamedCase("v1", "v2") + match value with + | NamedCase(name, optional) -> + name |> equal "v1" + optional |> equal "v2" + +[] +let ``Static union member returning the union type works`` () = + Example.All |> Array.length |> equal 2 + Example.All.[0] |> equal Example.First + Example.All.[1] |> equal Example.Second + +[] +let ``Static union member with nested self-reference works`` () = + Example.Head |> equal (Some Example.First) + Example.Nested |> List.head |> Array.head |> equal Example.First + Example.Pair |> fst |> equal Example.First + Example.Pair |> snd |> equal Example.Second + Example.Factory() |> equal Example.First + +[] +let ``Struct unions work`` () = + let distance p = + match p with + | Planar2D (x, y) -> sqrt (x * x + y * y) + | Infinity -> infinity + let p = Planar2D (3, 4) + let res = distance p + res |> equal 5. + +[] +let ``Struct unions with fields work`` () = + let radius = 3.0 + let circle = StructShape.Circle radius + let square = StructShape.Square(int (2.0 * radius)) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``Struct unions without fields work`` () = + let radius = 3.0 + let circle = StructShape2.Circle radius + let square = StructShape2.Square(2.0 * radius) + circle.Area |> equal (System.Math.PI * radius * radius) + square.Area |> equal (4.0 * radius * radius) + +[] +let ``Nested union match keeps case-specific field access`` () = + nestedUnionCaseAccess (NestedA 7) |> equal 0 + nestedUnionCaseAccess (NestedB 11) |> equal -1 + +[] +let ``Union case matching works`` () = + let x = IntCase1 5 + let res = + match x with + | IntCase1 a -> a + | IntCase2 b -> b + | IntCase3 c -> c + res |> equal 5 + +[] +let ``Union case equality works`` () = + IntCase1 5 = IntCase1 5 |> equal true + IntCase1 5 = IntCase2 5 |> equal false + IntCase3 2 = IntCase3 3 |> equal false + IntCase2 1 = IntCase2 1 |> equal true + IntCase3 1 = IntCase3 1 |> equal true + +[] +let ``Union fn call works`` () = + let x = IntCase1 3 + let res = unionFnAlways1 x + let res2 = unionFnAlways1 x //deliberately force clone/borrow break + let res3 = unionFnRetNum x + let res4 = unionFnRetNum (IntCase2 24) + res |> equal 3 + res2 |> equal 3 + res3 |> equal 3 + res4 |> equal 24 + +[] +let ``Union with wrapped type works`` () = + let a = AString "hello" + let b = match a with AString s -> s + " world" + b |> equal "hello world" + +[] +let ``Deep union with wrapped type works`` () = + let a = DeepWrappedA (" world", {Value = "hello"}) + let b = DeepWrappedB "world" + let c = DeepWrappedC 42 + let d = DeepWrappedD { Value = "hello" } + let f = DeepWrappedF (AString "doublewrapped") + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + a |> matchStrings |> equal "hello world" + b |> matchStrings |> equal "world" + c |> matchStrings |> equal "nothing" + d |> matchStrings |> equal "hello" + f |> matchStrings |> equal "doublewrapped" + g |> matchStrings |> equal "G" + +[] +let ``Deep union with tuped prim type works`` () = + let e = DeepWrappedE (3, 2) + let c = DeepWrappedC 42 //todo to string! + let g = DeepWrappedG {| X = { Value = "G" }; Y = 365 |} + e |> matchNumbers |> equal 5 + c |> matchNumbers |> equal 42 + g |> matchNumbers |> equal 365 + +[] +let ``Multi-case Union with wrapped type works`` () = + let b = DeepWrappedB "hello" + let res = match b with DeepWrappedB s -> s + " world" | _ -> "" + res |> equal "hello world" + +[] +let ``Match with condition works`` () = + let b1 = DeepWrappedB "hello" + let b2 = DeepWrappedB "not" + b1 |> matchStringWhenStringNotHello |> equal "hello" + b2 |> matchStringWhenStringNotHello |> equal "not hello" + +[] +let ``Should correctly import union in other file and allow creation`` () = + let r = Common.Imports.MyUnion.A 1 + let expected = Common.Imports.MyUnion.createA 1 + r |> equal expected + +#if FABLE_COMPILER +open Fable.Core + +[] +type ArcUnion = + | ArcS of string + | ArcI of int + +let add1 = function + | ArcS s -> s + "1" |> ArcS + | ArcI i -> i + 1 |> ArcI + +[] +let ``Union with pointer type works`` () = + let a = ArcS "hello" + let b = ArcI 42 + let aa = add1 a + let bb = add1 b + aa |> equal (ArcS "hello1") + bb |> equal (ArcI 43) + +[")>] +let ensureMyUnionWrapped s = nativeOnly + +[] +let ensureIsStructUnionUnwrapped s = nativeOnly + +[] +let ``Normal union should be wrapped in a Lrc`` () = + IntCase1 1 |> ensureMyUnionWrapped |> ignore + +[] +let ``Struct union should not be wrapped in a Lrc`` () = + Infinity |> ensureIsStructUnionUnwrapped |> ignore + +#endif From b210461c1651eaaa47e1027597c7545b6f2097fa Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 17:04:45 +0200 Subject: [PATCH 183/322] feat(standalone)!: keep file name in declaration locations (#5089) --- src/fable-standalone/src/Interfaces.fs | 11 +++++++++- src/fable-standalone/src/Main.fs | 21 +++++++++++++------ src/fable-standalone/src/Worker/Shared.fs | 2 +- src/fable-standalone/src/Worker/Worker.fs | 18 ++++++---------- src/fable-standalone/test/worker/README.md | 3 ++- src/fable-standalone/test/worker/run.mjs | 24 +++++++++++++++++++++- 6 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index f901f73536..34a0cf01ea 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -31,12 +31,21 @@ type Error = type Range = { + FileName: string StartLine: int StartColumn: int EndLine: int EndColumn: int } +[] +type DeclarationLocation = + /// The file is the one FCS recorded for the symbol, so it can be a source outside the project + | Found of Range + /// A .NET symbol from a referenced assembly, e.g. `System.Math.Abs` + | External of assembly: string * symbol: string + | NotFound of reason: string + type Completion = { Name: string @@ -159,7 +168,7 @@ type IFableManager = abstract GetErrors: parseResults: IParseAndCheckResults -> Error[] abstract GetDeclarationLocation: - parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Range option + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> DeclarationLocation abstract GetToolTips: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ToolTip[] diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index 522dd225db..6ae7e9ab00 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -345,28 +345,37 @@ let toToolTips (el: ToolTipElement) : Fable.Standalone.ToolTip[] = } |] -/// Get tool tip at the specified location +let private declarationNotFoundReason (reason: FindDeclFailureReason) = + match reason with + | FindDeclFailureReason.Unknown message -> message + | FindDeclFailureReason.NoSourceCode -> "no source code" + | FindDeclFailureReason.ProvidedType typeName -> "provided type " + typeName + | FindDeclFailureReason.ProvidedMember memberName -> "provided member " + memberName + +/// Get the declaration location of the symbol at the specified location let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> match findLongIdents (col - 1, lineText) with - | None -> None + | None -> DeclarationLocation.NotFound "no identifier at location" | Some(col, identIsland) -> let (declarations: FindDeclResult) = checkFile.GetDeclarationLocation(line, col, lineText, identIsland) match declarations with - | FindDeclResult.DeclNotFound _ - | FindDeclResult.ExternalDecl _ -> None + | FindDeclResult.DeclNotFound reason -> DeclarationLocation.NotFound(declarationNotFoundReason reason) + | FindDeclResult.ExternalDecl(assembly, externalSym) -> + DeclarationLocation.External(assembly, string externalSym) | FindDeclResult.DeclFound range -> - Some + DeclarationLocation.Found { + FileName = range.FileName StartLine = range.StartLine StartColumn = range.StartColumn EndLine = range.EndLine EndColumn = range.EndColumn } - | None -> None + | None -> DeclarationLocation.NotFound "file not checked" /// Get tool tip at the specified location let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index 0e6a1e7bb4..2fe797400d 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -74,7 +74,7 @@ type WorkerAnswer = | CompilerCrashed of message: string | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] - | FoundDeclarationLocation of id: Guid (* line1, col1, line2, col2 *) * (int * int * int * int) option + | FoundDeclarationLocation of id: Guid * Fable.Standalone.DeclarationLocation static member Decoder = Decode.Auto.generateDecoder() diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 675c87dc6b..7591894866 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -449,15 +449,12 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetDeclarationLocation(id, line, col, lineText) -> - let result = + let location = match FILE_NAME |> state.CurrentResults.TryFind with - | None -> None + | None -> DeclarationLocation.NotFound "file not parsed" | Some res -> fable.Manager.GetDeclarationLocation(res, int line, int col, lineText) - match result with - | Some x -> FoundDeclarationLocation(id, Some(x.StartLine, x.StartColumn, x.EndLine, x.EndColumn)) - | None -> FoundDeclarationLocation(id, None) - |> state.Worker.Post + FoundDeclarationLocation(id, location) |> state.Worker.Post return! loop box state @@ -480,15 +477,12 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetDeclarationLocationForFile(id, file, line, col, lineText) -> - let result = + let location = match file |> state.CurrentResults.TryFind with - | None -> None + | None -> DeclarationLocation.NotFound "file not parsed" | Some res -> fable.Manager.GetDeclarationLocation(res, int line, int col, lineText) - match result with - | Some x -> FoundDeclarationLocation(id, Some(x.StartLine, x.StartColumn, x.EndLine, x.EndColumn)) - | None -> FoundDeclarationLocation(id, None) - |> state.Worker.Post + FoundDeclarationLocation(id, location) |> state.Worker.Post return! loop box state } diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index eb78c4a1a3..ca3f346947 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -25,7 +25,8 @@ The fixture is generated, so nothing is downloaded. ## What it checks It hovers a library function and a documented user function, and checks that the tooltip signature -has no stray spaces and keeps the FCS tags and the XML doc. +has no stray spaces and keeps the FCS tags and the XML doc. It then asks for the declaration of a +project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the three stay apart. Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index be9fa158c0..55d41ecf07 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -171,7 +171,8 @@ console.log(" BEAM example hashing and state-key diagnostics checked") // not gain any, and the tags and the XML doc must come through separately. const greetLine = `let greet (name: string) = sprintf "Hello %s" name` const runLine = `let run () = greet "world"` -post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine].join("\n") }], []]) +const absLine = `let abs = System.Math.Abs(-1)` +post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine].join("\n") }], []]) await answer("ParsedCode") const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") @@ -193,6 +194,27 @@ if (!signature.some((t) => t.Tag === "keyword") || !signature.some((t) => t.Tag if (!greetTooltip[0]?.XmlDoc?.includes("")) failures.push(`tooltip lost the XML doc: ${JSON.stringify(greetTooltip[0]?.XmlDoc)}`) console.log(` tooltip: ${greetSignature} (${signature.length} tagged parts, ${greetTooltip[0]?.XmlDoc?.length ?? 0} doc lines)`) +// Declaration locations: a project symbol, an FSharp.Core symbol (whose recorded file is not in the +// project) and a .NET symbol must stay distinguishable. +const declarationOf = async (id, line, col, lineText) => { + post(["GetDeclarationLocationForFile", id, "Program.fs", line, col, lineText]) + const [, , location] = await answer("FoundDeclarationLocation") + return location +} +const greetDecl = await declarationOf("00000000-0000-0000-0000-000000000003", 4, 15, runLine) +const sprintfDecl = await declarationOf("00000000-0000-0000-0000-000000000004", 3, 29, greetLine) +const absDecl = await declarationOf("00000000-0000-0000-0000-000000000005", 5, 24, absLine) +if (greetDecl[0] !== "Found" || greetDecl[1].FileName !== "Program.fs" || greetDecl[1].StartLine !== 3) { + failures.push(`greet should be found in Program.fs: ${JSON.stringify(greetDecl)}`) +} +if (sprintfDecl[0] !== "Found" || sprintfDecl[1].FileName === "Program.fs") { + failures.push(`sprintf should be found in an FSharp.Core source: ${JSON.stringify(sprintfDecl)}`) +} +if (absDecl[0] !== "External" || absDecl[2] !== "System.Math.Abs(System.Int32)") { + failures.push(`Math.Abs should be an external .NET symbol: ${JSON.stringify(absDecl)}`) +} +console.log(` declarations: greet in ${greetDecl[1]?.FileName}, sprintf in ${sprintfDecl[1]?.FileName}, Math.Abs in ${absDecl[1]}`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) From 9eff071c8e5cf418e4bf9af63c051eae121d4512 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 17:38:24 +0200 Subject: [PATCH 184/322] feat(standalone): expose symbol lookup and uses in file (#5101) --- src/fable-standalone/src/Interfaces.fs | 15 ++++++ src/fable-standalone/src/Main.fs | 55 ++++++++++++++++++---- src/fable-standalone/src/Worker/Shared.fs | 3 ++ src/fable-standalone/src/Worker/Worker.fs | 22 +++++++++ src/fable-standalone/test/worker/README.md | 1 + src/fable-standalone/test/worker/run.mjs | 10 ++++ 6 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 34a0cf01ea..2bdd2f7bc2 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -111,6 +111,16 @@ type SourceMapping = int * int * int * int * string option type IChecker = interface end +/// Opaque FCS symbol, valid for the checker it came from +type ISymbol = + abstract DisplayName: string + +type SymbolUse = + { + Range: Range + IsDefinition: bool + } + /// Root modules and inline member bodies from a `fable precompile` output. type IPrecompiledInfo = /// Must be the name the precompiled assembly is referenced under, plus ".dll" @@ -167,6 +177,11 @@ type IFableManager = abstract GetErrors: parseResults: IParseAndCheckResults -> Error[] + abstract GetSymbolAtLocation: + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ISymbol option + + abstract GetUsesOfSymbolInFile: parseResults: IParseAndCheckResults * symbol: ISymbol -> SymbolUse[] + abstract GetDeclarationLocation: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> DeclarationLocation diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index 6ae7e9ab00..2e7a915525 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -352,6 +352,43 @@ let private declarationNotFoundReason (reason: FindDeclFailureReason) = | FindDeclFailureReason.ProvidedType typeName -> "provided type " + typeName | FindDeclFailureReason.ProvidedMember memberName -> "provided member " + memberName +let private toRange (range: FSharp.Compiler.Text.range) : Range = + { + FileName = range.FileName + StartLine = range.StartLine + StartColumn = range.StartColumn + EndLine = range.EndLine + EndColumn = range.EndColumn + } + +type private SymbolImpl(symbol: FSharpSymbol) = + member _.Symbol = symbol + + interface ISymbol with + member _.DisplayName = symbol.DisplayName + +let getSymbolAtLocation (results: ParseAndCheckResults) line col lineText = + match results.CheckFileResultsOpt with + | Some checkFile -> + match findLongIdents (col - 1, lineText) with + | None -> None + | Some(col, identIsland) -> + checkFile.GetSymbolUseAtLocation(line, col, lineText, identIsland) + |> Option.map (fun symbolUse -> SymbolImpl(symbolUse.Symbol) :> ISymbol) + | None -> None + +let getUsesOfSymbolInFile (results: ParseAndCheckResults) (symbol: ISymbol) = + match results.CheckFileResultsOpt, symbol with + | Some checkFile, (:? SymbolImpl as symbol) -> + checkFile.GetUsesOfSymbolInFile(symbol.Symbol) + |> Array.map (fun symbolUse -> + { + Range = toRange symbolUse.Range + IsDefinition = symbolUse.IsFromDefinition + } + ) + | _ -> [||] + /// Get the declaration location of the symbol at the specified location let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with @@ -366,15 +403,7 @@ let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = | FindDeclResult.DeclNotFound reason -> DeclarationLocation.NotFound(declarationNotFoundReason reason) | FindDeclResult.ExternalDecl(assembly, externalSym) -> DeclarationLocation.External(assembly, string externalSym) - | FindDeclResult.DeclFound range -> - DeclarationLocation.Found - { - FileName = range.FileName - StartLine = range.StartLine - StartColumn = range.StartColumn - EndLine = range.EndLine - EndColumn = range.EndColumn - } + | FindDeclResult.DeclFound range -> DeclarationLocation.Found(toRange range) | None -> DeclarationLocation.NotFound "file not checked" /// Get tool tip at the specified location @@ -597,6 +626,14 @@ let init () = member _.GetErrors(results: IParseAndCheckResults) = results.Errors + member _.GetSymbolAtLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = + let res = results :?> ParseAndCheckResults + getSymbolAtLocation res line col lineText + + member _.GetUsesOfSymbolInFile(results: IParseAndCheckResults, symbol: ISymbol) = + let res = results :?> ParseAndCheckResults + getUsesOfSymbolInFile res symbol + member _.GetDeclarationLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getDeclarationLocation res line col lineText diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index 2fe797400d..cb1d149365 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -52,9 +52,11 @@ type WorkerRequest = | GetTooltip of id: Guid * line: int * column: int * lineText: string | GetCompletions of id: Guid * line: int * column: int * lineText: string | GetDeclarationLocation of id: Guid * line: int * column: int * lineText: string + | GetSymbolUses of id: Guid * line: int * column: int * lineText: string | GetTooltipForFile of id: Guid * file: string * line: int * column: int * lineText: string | GetCompletionsForFile of id: Guid * file: string * line: int * column: int * lineText: string | GetDeclarationLocationForFile of id: Guid * file: string * line: int * column: int * lineText: string + | GetSymbolUsesForFile of id: Guid * file: string * line: int * column: int * lineText: string static member Decoder = Decode.Auto.generateDecoder() @@ -75,6 +77,7 @@ type WorkerAnswer = | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] | FoundDeclarationLocation of id: Guid * Fable.Standalone.DeclarationLocation + | FoundSymbolUses of id: Guid * uses: Fable.Standalone.SymbolUse[] static member Decoder = Decode.Auto.generateDecoder() diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 7591894866..d70631e17b 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -319,6 +319,14 @@ let private truncate (s: string) = else s +let private symbolUsesAt (fable: FableState) (results: IParseAndCheckResults option) line col lineText = + match results with + | None -> [||] + | Some res -> + match fable.Manager.GetSymbolAtLocation(res, line, col, lineText) with + | None -> [||] + | Some symbol -> fable.Manager.GetUsesOfSymbolInFile(res, symbol) + let rec loop (box: MailboxProcessor) (state: State) = async { @@ -448,6 +456,13 @@ let rec loop (box: MailboxProcessor) (state: State) = FoundCompletions(id, completions) |> state.Worker.Post return! loop box state + | Some fable, GetSymbolUses(id, line, col, lineText) -> + let uses = + symbolUsesAt fable (state.CurrentResults.TryFind FILE_NAME) (int line) (int col) lineText + + FoundSymbolUses(id, uses) |> state.Worker.Post + return! loop box state + | Some fable, GetDeclarationLocation(id, line, col, lineText) -> let location = match FILE_NAME |> state.CurrentResults.TryFind with @@ -476,6 +491,13 @@ let rec loop (box: MailboxProcessor) (state: State) = FoundCompletions(id, completions) |> state.Worker.Post return! loop box state + | Some fable, GetSymbolUsesForFile(id, file, line, col, lineText) -> + let uses = + symbolUsesAt fable (state.CurrentResults.TryFind file) (int line) (int col) lineText + + FoundSymbolUses(id, uses) |> state.Worker.Post + return! loop box state + | Some fable, GetDeclarationLocationForFile(id, file, line, col, lineText) -> let location = match file |> state.CurrentResults.TryFind with diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index ca3f346947..b85387911a 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -27,6 +27,7 @@ The fixture is generated, so nothing is downloaded. It hovers a library function and a documented user function, and checks that the tooltip signature has no stray spaces and keeps the FCS tags and the XML doc. It then asks for the declaration of a project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the three stay apart. +Finally it asks for every use of a project symbol and checks that the definition is marked. Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 55d41ecf07..4c8c2af154 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -215,6 +215,16 @@ if (absDecl[0] !== "External" || absDecl[2] !== "System.Math.Abs(System.Int32)") } console.log(` declarations: greet in ${greetDecl[1]?.FileName}, sprintf in ${sprintfDecl[1]?.FileName}, Math.Abs in ${absDecl[1]}`) +// Symbol uses: the definition of greet and its use must both come back, and nothing else. +post(["GetSymbolUsesForFile", "00000000-0000-0000-0000-000000000006", "Program.fs", 4, 15, runLine]) +const [, , greetUses] = await answer("FoundSymbolUses") +const usesText = greetUses.map((u) => `${u.Range.FileName}:${u.Range.StartLine}:${u.Range.StartColumn}-${u.Range.EndColumn}${u.IsDefinition ? " (definition)" : ""}`) +const definitions = greetUses.filter((u) => u.IsDefinition) +if (greetUses.length !== 2 || definitions.length !== 1 || definitions[0].Range.StartLine !== 3 || greetUses.some((u) => u.Range.FileName !== "Program.fs")) { + failures.push(`expected the definition and one use of greet: ${JSON.stringify(greetUses)}`) +} +console.log(` symbol uses of greet: ${usesText.join(", ")}`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) From 53849cc5a65f5de57dc65c76158b840e7012bc9c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 21:44:40 +0200 Subject: [PATCH 185/322] feat(standalone)!: return full completions with lazy docs (#5102) --- src/fable-standalone/src/Interfaces.fs | 29 +++++++++++++++-- src/fable-standalone/src/Main.fs | 38 ++++++++++++++++++---- src/fable-standalone/src/Worker/Shared.fs | 3 ++ src/fable-standalone/src/Worker/Worker.fs | 26 +++++++++++---- src/fable-standalone/test/worker/README.md | 1 + src/fable-standalone/test/worker/run.mjs | 23 ++++++++++++- 6 files changed, 104 insertions(+), 16 deletions(-) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 2bdd2f7bc2..5670cd9b23 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -46,10 +46,28 @@ type DeclarationLocation = | External of assembly: string * symbol: string | NotFound of reason: string +[] +type CompletionKind = + | SuggestedName + | Field + | Property + | Method + | ExtensionMethod + | Event + | Argument + | CustomOperation + | Other + type Completion = { - Name: string + NameInList: string + /// Text to insert, e.g. with backticks for names that need them + NameInCode: string Glyph: Glyph + Kind: CompletionKind + FullName: string + IsOwnMember: bool + MinorPriority: int } /// Mirrors FCS `FSharp.Compiler.Text.TextTag` @@ -107,6 +125,13 @@ type ToolTip = Remarks: TaggedText[] } +type CompletionItem = + { + Completion: Completion + /// Formats the tooltips on every call, so only call it for the highlighted row + GetDescription: unit -> ToolTip[] + } + type SourceMapping = int * int * int * int * string option type IChecker = interface end @@ -188,7 +213,7 @@ type IFableManager = abstract GetToolTips: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ToolTip[] abstract GetCompletionsAtLocation: - parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> Completion[] + parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> CompletionItem[] abstract CompileToTargetAst: fableLibrary: string * diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index 2e7a915525..f8c6494b65 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -425,6 +425,36 @@ let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = List.map toToolTips els |> Array.concat | None -> [||] +let private toCompletionKind (kind: CompletionItemKind) = + match kind with + | CompletionItemKind.SuggestedName -> CompletionKind.SuggestedName + | CompletionItemKind.Field -> CompletionKind.Field + | CompletionItemKind.Property -> CompletionKind.Property + | CompletionItemKind.Method false -> CompletionKind.Method + | CompletionItemKind.Method true -> CompletionKind.ExtensionMethod + | CompletionItemKind.Event -> CompletionKind.Event + | CompletionItemKind.Argument -> CompletionKind.Argument + | CompletionItemKind.CustomOperation -> CompletionKind.CustomOperation + | CompletionItemKind.Other -> CompletionKind.Other + +let private toCompletionItem (decl: DeclarationListItem) : Fable.Standalone.CompletionItem = + { + Completion = + { + NameInList = decl.NameInList + NameInCode = decl.NameInCode + Glyph = convertGlyph decl.Glyph + Kind = toCompletionKind decl.Kind + FullName = decl.FullName + IsOwnMember = decl.IsOwnMember + MinorPriority = decl.MinorPriority + } + GetDescription = + fun () -> + let (ToolTipText els) = decl.Description + List.map toToolTips els |> Array.concat + } + let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: int) lineText = match results.CheckFileResultsOpt with | Some checkFile -> @@ -440,13 +470,7 @@ let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: i let decls = checkFile.GetDeclarationListInfo(results.ParseFileResultsOpt, line, lineText, longName, (fun () -> [])) - decls.Items - |> Array.map (fun decl -> - { - Name = decl.NameInList - Glyph = convertGlyph decl.Glyph - } - ) + decls.Items |> Array.map toCompletionItem | None -> [||] let mapFableError (com: Compiler) (log: LogEntry) = diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index cb1d149365..fa25b0b792 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -51,6 +51,8 @@ type WorkerRequest = otherFSharpOptions: string[] | GetTooltip of id: Guid * line: int * column: int * lineText: string | GetCompletions of id: Guid * line: int * column: int * lineText: string + /// Tooltips of an item of the last FoundCompletions answer + | GetCompletionDescription of id: Guid * index: int | GetDeclarationLocation of id: Guid * line: int * column: int * lineText: string | GetSymbolUses of id: Guid * line: int * column: int * lineText: string | GetTooltipForFile of id: Guid * file: string * line: int * column: int * lineText: string @@ -76,6 +78,7 @@ type WorkerAnswer = | CompilerCrashed of message: string | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] + | FoundCompletionDescription of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundDeclarationLocation of id: Guid * Fable.Standalone.DeclarationLocation | FoundSymbolUses of id: Guid * uses: Fable.Standalone.SymbolUse[] diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index d70631e17b..67c8421ecb 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -65,6 +65,7 @@ type State = Fable: FableState option Worker: ObservableWorker CurrentResults: Map + CurrentCompletions: CompletionItem[] } /// Map a REPL language string to its fable-library directory name (used as the @@ -448,13 +449,14 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetCompletions(id, line, col, lineText) -> - let completions = + let items = match FILE_NAME |> state.CurrentResults.TryFind with | None -> [||] | Some res -> fable.Manager.GetCompletionsAtLocation(res, int line, int col, lineText) - FoundCompletions(id, completions) |> state.Worker.Post - return! loop box state + FoundCompletions(id, items |> Array.map _.Completion) |> state.Worker.Post + + return! loop box { state with CurrentCompletions = items } | Some fable, GetSymbolUses(id, line, col, lineText) -> let uses = @@ -463,6 +465,15 @@ let rec loop (box: MailboxProcessor) (state: State) = FoundSymbolUses(id, uses) |> state.Worker.Post return! loop box state + | Some _, GetCompletionDescription(id, index) -> + let tooltips = + match Array.tryItem index state.CurrentCompletions with + | None -> [||] + | Some item -> item.GetDescription() + + FoundCompletionDescription(id, tooltips) |> state.Worker.Post + return! loop box state + | Some fable, GetDeclarationLocation(id, line, col, lineText) -> let location = match FILE_NAME |> state.CurrentResults.TryFind with @@ -483,13 +494,15 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state | Some fable, GetCompletionsForFile(id, file, line, col, lineText) -> - let completions = + let items = match file |> state.CurrentResults.TryFind with | None -> [||] | Some res -> fable.Manager.GetCompletionsAtLocation(res, int line, int col, lineText) - FoundCompletions(id, completions) |> state.Worker.Post - return! loop box state + FoundCompletions(id, items |> Array.map (fun item -> item.Completion)) + |> state.Worker.Post + + return! loop box { state with CurrentCompletions = items } | Some fable, GetSymbolUsesForFile(id, file, line, col, lineText) -> let uses = @@ -517,6 +530,7 @@ let box = Fable = None Worker = worker CurrentResults = Map.empty + CurrentCompletions = [||] } |> loop box ) diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index b85387911a..27d139279a 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -28,6 +28,7 @@ It hovers a library function and a documented user function, and checks that the has no stray spaces and keeps the FCS tags and the XML doc. It then asks for the declaration of a project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the three stay apart. Finally it asks for every use of a project symbol and checks that the definition is marked. +It also lists completions and fetches the description of one of them by index. Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 4c8c2af154..c89553d5ae 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -172,7 +172,8 @@ console.log(" BEAM example hashing and state-key diagnostics checked") const greetLine = `let greet (name: string) = sprintf "Hello %s" name` const runLine = `let run () = greet "world"` const absLine = `let abs = System.Math.Abs(-1)` -post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine].join("\n") }], []]) +const greLine = `let g = gre` +post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine, greLine].join("\n") }], []]) await answer("ParsedCode") const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") @@ -225,6 +226,26 @@ if (greetUses.length !== 2 || definitions.length !== 1 || definitions[0].Range.S } console.log(` symbol uses of greet: ${usesText.join(", ")}`) +// Completions: the full item must come back, and the description of an item must be fetchable +// afterwards by index. +const completionsStarted = Date.now() +post(["GetCompletionsForFile", "00000000-0000-0000-0000-000000000007", "Program.fs", 6, 12, greLine]) +const [, , greCompletions] = await answer("FoundCompletions") +const completionsMs = Date.now() - completionsStarted +const greetIndex = greCompletions.findIndex((c) => c.NameInList === "greet") +const greet = greCompletions[greetIndex] +if (!greet || greet.NameInCode !== "greet" || greet.FullName !== "Demo.greet") { + failures.push(`greet should be a completion of the project: ${JSON.stringify(greet)}`) +} +console.log(` completions for "${greLine}": ${greCompletions.length} items in ${completionsMs}ms`) + +post(["GetCompletionDescription", "00000000-0000-0000-0000-000000000008", greetIndex]) +const [, , greetDescription] = await answer("FoundCompletionDescription") +if (!greetDescription[0]?.XmlDoc?.includes(" Says hello.")) { + failures.push(`completion description lost the XML doc: ${JSON.stringify(greetDescription)}`) +} +console.log(` completion description of greet: ${signatureOf(greetDescription)} (${greetDescription[0]?.XmlDoc?.length ?? 0} doc lines)`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) From 4ec83ff4e280db3075230e72b02b699a16f2bc76 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 22:02:11 +0200 Subject: [PATCH 186/322] feat(standalone)!: report error codes and severities (#5103) --- src/fable-compiler-js/src/app.fs | 17 +++++++++----- src/fable-standalone/src/Interfaces.fs | 11 ++++++++- src/fable-standalone/src/Main.fs | 26 ++++++++++------------ src/fable-standalone/src/Worker/Worker.fs | 12 ++++++++-- src/fable-standalone/test/worker/README.md | 1 + src/fable-standalone/test/worker/run.mjs | 25 ++++++++++++++++----- 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/fable-compiler-js/src/app.fs b/src/fable-compiler-js/src/app.fs index 3b077acd13..400e532611 100644 --- a/src/fable-compiler-js/src/app.fs +++ b/src/fable-compiler-js/src/app.fs @@ -185,14 +185,19 @@ type SourceWriter(sourcePath, targetPath, projDir, options: CmdLineOptions, file let printErrors showWarnings (errors: Fable.Standalone.Error[]) = let printError (e: Fable.Standalone.Error) = let errorType = - if e.IsWarning then - "Warning" - else - "Error" + match e.Severity with + | Fable.Standalone.DiagnosticSeverity.Hidden + | Fable.Standalone.DiagnosticSeverity.Info -> "Info" + | Fable.Standalone.DiagnosticSeverity.Warning -> "Warning" + | Fable.Standalone.DiagnosticSeverity.Error -> "Error" + + printfn $"%s{e.FileName} (%d{e.StartLine},%d{e.StartColumn}): %s{errorType} %s{e.Code}: %s{e.Message}" - printfn $"%s{e.FileName} (%d{e.StartLine},%d{e.StartColumn}): %s{errorType}: %s{e.Message}" + let warnings, errors = + errors + |> Array.filter (fun e -> e.Severity <> Fable.Standalone.DiagnosticSeverity.Hidden) + |> Array.partition (fun e -> e.Severity <> Fable.Standalone.DiagnosticSeverity.Error) - let warnings, errors = errors |> Array.partition (fun e -> e.IsWarning) let hasErrors = not (Array.isEmpty errors) if showWarnings then diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 5670cd9b23..66e896bb3a 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -18,6 +18,13 @@ type Glyph = | Event | TypeParameter +[] +type DiagnosticSeverity = + | Hidden + | Info + | Warning + | Error + type Error = { FileName: string @@ -26,7 +33,9 @@ type Error = EndLine: int EndColumn: int Message: string - IsWarning: bool + Severity: DiagnosticSeverity + /// FCS error number such as "FS0039", or the Fable log tag such as "FABLE" + Code: string } type Range = diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index f8c6494b65..d1544b11a5 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -24,12 +24,13 @@ let mapError (error: FSharpDiagnostic) = EndLine = error.EndLine EndColumn = error.EndColumn Message = error.Message - IsWarning = + Severity = match error.Severity with - | FSharpDiagnosticSeverity.Info - | FSharpDiagnosticSeverity.Hidden - | FSharpDiagnosticSeverity.Warning -> true - | FSharpDiagnosticSeverity.Error -> false + | FSharpDiagnosticSeverity.Hidden -> DiagnosticSeverity.Hidden + | FSharpDiagnosticSeverity.Info -> DiagnosticSeverity.Info + | FSharpDiagnosticSeverity.Warning -> DiagnosticSeverity.Warning + | FSharpDiagnosticSeverity.Error -> DiagnosticSeverity.Error + Code = error.ErrorNumberText } type ParseAndCheckResults @@ -482,16 +483,13 @@ let mapFableError (com: Compiler) (log: LogEntry) = StartColumn = r.start.column EndLine = r.``end``.line EndColumn = r.``end``.column - Message = - if log.Tag = "FABLE" then - "FABLE: " + log.Message - else - log.Message - IsWarning = + Message = log.Message + Severity = match log.Severity with - | Fable.Severity.Error -> false - | Fable.Severity.Warning - | Fable.Severity.Info -> true + | Fable.Severity.Info -> DiagnosticSeverity.Info + | Fable.Severity.Warning -> DiagnosticSeverity.Warning + | Fable.Severity.Error -> DiagnosticSeverity.Error + Code = log.Tag } type BabelResult(ast: Babel.Program, errors) = diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index 67c8421ecb..d989d8431f 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -247,7 +247,10 @@ let private compileCode fable fileName fsharpNames fsharpCodes language otherFSh let! compiledCode, errors, fableTransformTime = async { - if parseResults.Errors |> Array.exists (fun e -> not e.IsWarning) then + if + parseResults.Errors + |> Array.exists (fun e -> e.Severity = DiagnosticSeverity.Error) + then return "", parseResults.Errors, 0. else let! code, fableErrors, fableTransformTime = @@ -290,7 +293,12 @@ let private compileFiles fable fsharpNames fsharpCodes (filesToEmit: string[]) l let mutable fableTransformTime = 0. let compiledCode = ResizeArray() - if not (parseResults.Errors |> Array.exists (fun e -> not e.IsWarning)) then + if + not ( + parseResults.Errors + |> Array.exists (fun e -> e.Severity = DiagnosticSeverity.Error) + ) + then for fileName in filesToEmit do let! code, fableErrors, transformTime = emitFile fable parseResults fileName language fableOptions diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index 27d139279a..99f1c88d19 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -29,6 +29,7 @@ has no stray spaces and keeps the FCS tags and the XML doc. It then asks for the project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the three stay apart. Finally it asks for every use of a project symbol and checks that the definition is marked. It also lists completions and fetches the description of one of them by index. +The parsed fixture carries one warning and one error, checked for their FCS code and severity. Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index c89553d5ae..7ba35b0283 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -114,7 +114,7 @@ console.log(` ${modules.length} modules in ${((Date.now() - started) / 1000).to const failures = [] if (modules.length !== fixture.length) failures.push(`expected ${fixture.length} modules, got ${modules.length}`) -const hard = (errors ?? []).filter((e) => !e.IsWarning) +const hard = (errors ?? []).filter((e) => e.Severity === "error") if (hard.length) failures.push(`${hard.length} compile error(s), first: ${hard[0].Message}`) // V8 caps the stack in bytes, so a frame count is only comparable against another count from this @@ -137,7 +137,7 @@ console.log(`re-emit ${last} only: ${subset.length} module ` + if (subset.length !== 1) failures.push(`expected 1 module for a single-file emit, got ${subset.length}`) if (subset[0] !== modules[modules.length - 1]) failures.push(`re-emitting ${last} produced different output`) -if ((subsetErrors ?? []).filter((e) => !e.IsWarning).length) failures.push("subset emit reported errors") +if ((subsetErrors ?? []).filter((e) => e.Severity === "error").length) failures.push("subset emit reported errors") if (subsetStats.Fable_transform >= stats.Fable_transform) { failures.push(`emitting 1 of ${fixture.length} files took as long as emitting all of them`) } @@ -146,7 +146,7 @@ if (subsetStats.Fable_transform >= stats.Fable_transform) { const longFunction = "long" + "x".repeat(230) post(["CompileFiles", [{ Name: "Program.fs", Content: `module Demo\nlet ${longFunction} () = 42` }], [], "beam", []]) const [, beamModules, , beamErrors] = await answer("CompilationsFinished") -const beamHard = (beamErrors ?? []).filter((e) => !e.IsWarning) +const beamHard = (beamErrors ?? []).filter((e) => e.Severity === "error") if (beamHard.length) failures.push(`BEAM example failed: ${beamHard[0].Message}`) const beamCode = beamModules.join("\n") if (!/longx+_[0-9a-f]{8}\(/.test(beamCode)) { @@ -162,7 +162,7 @@ post(["CompileFiles", [{ Name: "Program.fs", Content: [ `let read () = ${mutablePrefix}a2, ${mutablePrefix}cp`, ].join("\n") }], [], "beam", []]) const [, , , collisionErrors] = await answer("CompilationsFinished") -if (!(collisionErrors ?? []).some((e) => !e.IsWarning && e.Message.includes("share Erlang state key"))) { +if (!(collisionErrors ?? []).some((e) => e.Severity === "error" && e.Message.includes("share Erlang state key"))) { failures.push("standalone BEAM did not diagnose colliding mutable state keys") } console.log(" BEAM example hashing and state-key diagnostics checked") @@ -173,8 +173,21 @@ const greetLine = `let greet (name: string) = sprintf "Hello %s" name` const runLine = `let run () = greet "world"` const absLine = `let abs = System.Math.Abs(-1)` const greLine = `let g = gre` -post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine, greLine].join("\n") }], []]) -await answer("ParsedCode") +const genericLine = `let addOne (x: 'a) = x + 1` +const undefinedLine = `let bad = undefinedThing` +post(["ParseFile", "Program.fs", [{ Name: "Program.fs", Content: ["module Demo", "/// Says hello.", greetLine, runLine, absLine, greLine, genericLine, undefinedLine].join("\n") }], []]) +const [, parsedErrors] = await answer("ParsedCode") + +// Diagnostics: the FCS error number and the real severity must come through. +const diagnostics = parsedErrors.map((e) => `${e.Code}:${e.Severity}@${e.StartLine}`) +const fixtureLines = 6 +if (!parsedErrors.some((e) => e.Code === "FS0064" && e.Severity === "warning" && e.StartLine === fixtureLines + 1)) { + failures.push(`expected the FS0064 warning: ${diagnostics.join(", ")}`) +} +if (!parsedErrors.some((e) => e.Code === "FS0039" && e.Severity === "error" && e.StartLine === fixtureLines + 2)) { + failures.push(`expected the FS0039 error: ${diagnostics.join(", ")}`) +} +console.log(` diagnostics: ${diagnostics.join(", ")}`) const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") From e65d42c03b8262b94e058354904f1126560443b4 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 23:03:19 +0200 Subject: [PATCH 187/322] feat(standalone): expose lexer tokens and classification (#5110) --- src/fable-standalone/src/Interfaces.fs | 76 +++++++++++++++++ src/fable-standalone/src/Main.fs | 99 ++++++++++++++++++++++ src/fable-standalone/src/Worker/Shared.fs | 5 ++ src/fable-standalone/src/Worker/Worker.fs | 22 +++++ src/fable-standalone/test/worker/README.md | 2 + src/fable-standalone/test/worker/run.mjs | 23 +++++ 6 files changed, 227 insertions(+) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 66e896bb3a..bbf69bc39a 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -141,6 +141,77 @@ type CompletionItem = GetDescription: unit -> ToolTip[] } +[] +type TokenColor = + | Default + | Keyword + | Comment + | Identifier + | String + | UpperIdentifier + | InactiveCode + | PreprocessorKeyword + | Number + | Operator + | Punctuation + +/// A lexical token of one line. Columns are 0-based and the end is exclusive. +type Token = + { + StartColumn: int + EndColumn: int + Color: TokenColor + /// FCS token name such as "IDENT" or "STRING_TEXT" + Name: string + } + +/// Mirrors FCS `SemanticClassificationType` +[] +type SemanticClass = + | ReferenceType + | ValueType + | UnionCase + | UnionCaseField + | Function + | Property + | MutableVar + | Module + | Namespace + | Printf + | ComputationExpression + | IntrinsicFunction + | Enumeration + | Interface + | TypeArgument + | Operator + | DisposableType + | DisposableTopLevelValue + | DisposableLocalValue + | Method + | ExtensionMethod + | ConstructorForReferenceType + | ConstructorForValueType + | Literal + | RecordField + | MutableRecordField + | RecordFieldAsFunction + | Exception + | Field + | Event + | Delegate + | NamedArgument + | Value + | LocalValue + | Type + | TypeDef + | Plaintext + +type SemanticClassification = + { + Range: Range + Type: SemanticClass + } + type SourceMapping = int * int * int * int * string option type IChecker = interface end @@ -221,6 +292,11 @@ type IFableManager = abstract GetToolTips: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ToolTip[] + /// Lexical tokens of every line of the source, with `#if` blocks resolved against the defines + abstract Tokenize: source: string * defines: string[] -> Token[][] + + abstract GetSemanticClassification: parseResults: IParseAndCheckResults -> SemanticClassification[] + abstract GetCompletionsAtLocation: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> CompletionItem[] diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index d1544b11a5..78a72020d6 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -11,6 +11,7 @@ open FSharp.Compiler.Diagnostics open FSharp.Compiler.EditorServices open FSharp.Compiler.SourceCodeServices open FSharp.Compiler.Symbols +open FSharp.Compiler.Tokenization type CheckerImpl(checker: InteractiveChecker) = member _.Checker = checker @@ -407,6 +408,98 @@ let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = | FindDeclResult.DeclFound range -> DeclarationLocation.Found(toRange range) | None -> DeclarationLocation.NotFound "file not checked" +let private toTokenColor (color: FSharpTokenColorKind) = + match color with + | FSharpTokenColorKind.Keyword -> TokenColor.Keyword + | FSharpTokenColorKind.Comment -> TokenColor.Comment + | FSharpTokenColorKind.Identifier -> TokenColor.Identifier + | FSharpTokenColorKind.String -> TokenColor.String + | FSharpTokenColorKind.UpperIdentifier -> TokenColor.UpperIdentifier + | FSharpTokenColorKind.InactiveCode -> TokenColor.InactiveCode + | FSharpTokenColorKind.PreprocessorKeyword -> TokenColor.PreprocessorKeyword + | FSharpTokenColorKind.Number -> TokenColor.Number + | FSharpTokenColorKind.Operator -> TokenColor.Operator + | FSharpTokenColorKind.Punctuation -> TokenColor.Punctuation + | _ -> TokenColor.Default + +let tokenize (source: string) (defines: string[]) : Token[][] = + let tokenizer = FSharpSourceTokenizer(List.ofArray defines, None, None, None) + + let tokenizeLine (state: FSharpTokenizerLexState) (line: string) = + let lineTokenizer = tokenizer.CreateLineTokenizer(line.TrimEnd('\r')) + + let rec loop state acc = + match lineTokenizer.ScanToken state with + | Some token, state -> + let token: Token = + { + StartColumn = token.LeftColumn + EndColumn = token.RightColumn + 1 + Color = toTokenColor token.ColorClass + Name = token.TokenName + } + + loop state (token :: acc) + | None, state -> List.rev acc |> List.toArray, state + + loop state [] + + source.Split('\n') + |> Array.mapFold tokenizeLine FSharpTokenizerLexState.Initial + |> fst + +let private toSemanticClass (classification: SemanticClassificationType) = + match classification with + | SemanticClassificationType.ReferenceType -> SemanticClass.ReferenceType + | SemanticClassificationType.ValueType -> SemanticClass.ValueType + | SemanticClassificationType.UnionCase -> SemanticClass.UnionCase + | SemanticClassificationType.UnionCaseField -> SemanticClass.UnionCaseField + | SemanticClassificationType.Function -> SemanticClass.Function + | SemanticClassificationType.Property -> SemanticClass.Property + | SemanticClassificationType.MutableVar -> SemanticClass.MutableVar + | SemanticClassificationType.Module -> SemanticClass.Module + | SemanticClassificationType.Namespace -> SemanticClass.Namespace + | SemanticClassificationType.Printf -> SemanticClass.Printf + | SemanticClassificationType.ComputationExpression -> SemanticClass.ComputationExpression + | SemanticClassificationType.IntrinsicFunction -> SemanticClass.IntrinsicFunction + | SemanticClassificationType.Enumeration -> SemanticClass.Enumeration + | SemanticClassificationType.Interface -> SemanticClass.Interface + | SemanticClassificationType.TypeArgument -> SemanticClass.TypeArgument + | SemanticClassificationType.Operator -> SemanticClass.Operator + | SemanticClassificationType.DisposableType -> SemanticClass.DisposableType + | SemanticClassificationType.DisposableTopLevelValue -> SemanticClass.DisposableTopLevelValue + | SemanticClassificationType.DisposableLocalValue -> SemanticClass.DisposableLocalValue + | SemanticClassificationType.Method -> SemanticClass.Method + | SemanticClassificationType.ExtensionMethod -> SemanticClass.ExtensionMethod + | SemanticClassificationType.ConstructorForReferenceType -> SemanticClass.ConstructorForReferenceType + | SemanticClassificationType.ConstructorForValueType -> SemanticClass.ConstructorForValueType + | SemanticClassificationType.Literal -> SemanticClass.Literal + | SemanticClassificationType.RecordField -> SemanticClass.RecordField + | SemanticClassificationType.MutableRecordField -> SemanticClass.MutableRecordField + | SemanticClassificationType.RecordFieldAsFunction -> SemanticClass.RecordFieldAsFunction + | SemanticClassificationType.Exception -> SemanticClass.Exception + | SemanticClassificationType.Field -> SemanticClass.Field + | SemanticClassificationType.Event -> SemanticClass.Event + | SemanticClassificationType.Delegate -> SemanticClass.Delegate + | SemanticClassificationType.NamedArgument -> SemanticClass.NamedArgument + | SemanticClassificationType.Value -> SemanticClass.Value + | SemanticClassificationType.LocalValue -> SemanticClass.LocalValue + | SemanticClassificationType.Type -> SemanticClass.Type + | SemanticClassificationType.TypeDef -> SemanticClass.TypeDef + | _ -> SemanticClass.Plaintext + +let getSemanticClassification (results: ParseAndCheckResults) : SemanticClassification[] = + match results.CheckFileResultsOpt with + | Some checkFile -> + checkFile.GetSemanticClassification(None) + |> Array.map (fun item -> + { + Range = toRange item.Range + Type = toSemanticClass item.Type + } + ) + | None -> [||] + /// Get tool tip at the specified location let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with @@ -664,6 +757,12 @@ let init () = let res = results :?> ParseAndCheckResults getToolTipAtLocation res line col lineText + member _.Tokenize(source: string, defines: string[]) = tokenize source defines + + member _.GetSemanticClassification(results: IParseAndCheckResults) = + let res = results :?> ParseAndCheckResults + getSemanticClassification res + member _.GetCompletionsAtLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getCompletionsAtLocation res line col lineText diff --git a/src/fable-standalone/src/Worker/Shared.fs b/src/fable-standalone/src/Worker/Shared.fs index fa25b0b792..4ba682e75e 100644 --- a/src/fable-standalone/src/Worker/Shared.fs +++ b/src/fable-standalone/src/Worker/Shared.fs @@ -49,6 +49,9 @@ type WorkerRequest = filesToEmit: string[] * language: string * otherFSharpOptions: string[] + | Tokenize of id: Guid * source: string * defines: string[] + | GetSemanticClassification of id: Guid + | GetSemanticClassificationForFile of id: Guid * file: string | GetTooltip of id: Guid * line: int * column: int * lineText: string | GetCompletions of id: Guid * line: int * column: int * lineText: string /// Tooltips of an item of the last FoundCompletions answer @@ -76,6 +79,8 @@ type WorkerAnswer = | CompilationFinished of code: string * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilationsFinished of code: string[] * language: string * errors: Fable.Standalone.Error[] * stats: CompileStats | CompilerCrashed of message: string + | Tokenized of id: Guid * lines: Fable.Standalone.Token[][] + | FoundSemanticClassification of id: Guid * items: Fable.Standalone.SemanticClassification[] | FoundTooltip of id: Guid * tooltips: Fable.Standalone.ToolTip[] | FoundCompletions of id: Guid * Fable.Standalone.Completion[] | FoundCompletionDescription of id: Guid * tooltips: Fable.Standalone.ToolTip[] diff --git a/src/fable-standalone/src/Worker/Worker.fs b/src/fable-standalone/src/Worker/Worker.fs index d989d8431f..99d4336f81 100644 --- a/src/fable-standalone/src/Worker/Worker.fs +++ b/src/fable-standalone/src/Worker/Worker.fs @@ -447,6 +447,28 @@ let rec loop (box: MailboxProcessor) (state: State) = return! loop box state + | Some fable, Tokenize(id, source, defines) -> + Tokenized(id, fable.Manager.Tokenize(source, defines)) |> state.Worker.Post + return! loop box state + + | Some fable, GetSemanticClassification id -> + let items = + match FILE_NAME |> state.CurrentResults.TryFind with + | None -> [||] + | Some res -> fable.Manager.GetSemanticClassification(res) + + FoundSemanticClassification(id, items) |> state.Worker.Post + return! loop box state + + | Some fable, GetSemanticClassificationForFile(id, file) -> + let items = + match file |> state.CurrentResults.TryFind with + | None -> [||] + | Some res -> fable.Manager.GetSemanticClassification(res) + + FoundSemanticClassification(id, items) |> state.Worker.Post + return! loop box state + | Some fable, GetTooltip(id, line, col, lineText) -> let tooltips = match FILE_NAME |> state.CurrentResults.TryFind with diff --git a/src/fable-standalone/test/worker/README.md b/src/fable-standalone/test/worker/README.md index 99f1c88d19..532d1cf849 100644 --- a/src/fable-standalone/test/worker/README.md +++ b/src/fable-standalone/test/worker/README.md @@ -30,6 +30,8 @@ project symbol, an FSharp.Core symbol and a .NET symbol, and checks that the thr Finally it asks for every use of a project symbol and checks that the definition is marked. It also lists completions and fetches the description of one of them by index. The parsed fixture carries one warning and one error, checked for their FCS code and severity. +Finally it tokenizes a snippet with a multi-line string and an inactive `#if` block, and asks for the +semantic classification of the parsed fixture. Beyond "the compile finished and returned a module per file", it measures the **stack headroom the compiler is handed for each file** and fails if it drifts between files. diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 7ba35b0283..666359e847 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -259,6 +259,29 @@ if (!greetDescription[0]?.XmlDoc?.includes(" Says hello.")) { } console.log(` completion description of greet: ${signatureOf(greetDescription)} (${greetDescription[0]?.XmlDoc?.length ?? 0} doc lines)`) +// Tokens and semantic classification: the lexer must carry state across lines, and the checker +// must classify identifiers by what they are. +post(["Tokenize", "00000000-0000-0000-0000-000000000010", ["let s = \"\"\"multi", "line\"\"\" // done", "#if FOO", "let hidden = 1", "#endif"].join("\n"), []]) +const [, , tokenLines] = await answer("Tokenized") +const tokenText = tokenLines.map((line) => line.map((t) => `${t.Color}:${t.StartColumn}-${t.EndColumn}`).join(" ")) +if (tokenLines[0]?.[0]?.Color !== "keyword" || tokenLines[0][0].EndColumn !== 3) failures.push(`first token should be the keyword let: ${tokenText[0]}`) +if (!tokenLines[1]?.some((t) => t.Color === "string") || !tokenLines[1]?.some((t) => t.Color === "comment")) { + failures.push(`second line should continue the string and end with a comment: ${tokenText[1]}`) +} +if (!tokenLines[3]?.every((t) => t.Color === "inactiveCode")) failures.push(`code under #if FOO should be inactive: ${tokenText[3]}`) +console.log(` tokens: ${tokenText.join(" | ")}`) + +post(["GetSemanticClassificationForFile", "00000000-0000-0000-0000-000000000011", "Program.fs"]) +const [, , classified] = await answer("FoundSemanticClassification") +const classifiedText = classified.map((c) => `${c.Type}@${c.Range.StartLine}:${c.Range.StartColumn}-${c.Range.EndColumn}`) +if (!classified.some((c) => c.Type === "function" && c.Range.StartLine === 3 && c.Range.StartColumn === 4 && c.Range.EndColumn === 9)) { + failures.push(`greet should be classified as a function: ${classifiedText.join(", ")}`) +} +if (!classified.some((c) => c.Type === "referenceType" && c.Range.StartLine === 3 && c.Range.StartColumn === 17 && c.Range.EndColumn === 23)) { + failures.push(`the string annotation should be classified as a reference type: ${classifiedText.join(", ")}`) +} +console.log(` classification: ${classifiedText.join(", ")}`) + if (failures.length) { console.error("\nFAILED") for (const f of failures) console.error(` - ${f}`) From ec72286d3528d1bbc1bede72df11deb3432040b4 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 23:09:29 +0200 Subject: [PATCH 188/322] refactor(standalone)!: drop GetErrors, use Errors on results (#5111) --- src/fable-standalone/src/Interfaces.fs | 2 -- src/fable-standalone/src/Main.fs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index bbf69bc39a..38a32d9955 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -280,8 +280,6 @@ type IFableManager = ?precompiledInfo: IPrecompiledInfo -> IParseAndCheckResults - abstract GetErrors: parseResults: IParseAndCheckResults -> Error[] - abstract GetSymbolAtLocation: parseResults: IParseAndCheckResults * line: int * col: int * lineText: string -> ISymbol option diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index 78a72020d6..e82d1571a0 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -739,8 +739,6 @@ let init () = precompiledInfo :> IParseAndCheckResults - member _.GetErrors(results: IParseAndCheckResults) = results.Errors - member _.GetSymbolAtLocation(results: IParseAndCheckResults, line: int, col: int, lineText: string) = let res = results :?> ParseAndCheckResults getSymbolAtLocation res line col lineText From e33c57870454fea189a3eb33519f583a5003f8c3 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 16 Oct 2026 23:09:45 +0200 Subject: [PATCH 189/322] feat(standalone)!: take 0-based columns in positional APIs (#5112) --- src/fable-standalone/src/Interfaces.fs | 4 ++++ src/fable-standalone/src/Main.fs | 10 +++++----- src/fable-standalone/test/worker/run.mjs | 16 +++++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/fable-standalone/src/Interfaces.fs b/src/fable-standalone/src/Interfaces.fs index 38a32d9955..2e5a1760de 100644 --- a/src/fable-standalone/src/Interfaces.fs +++ b/src/fable-standalone/src/Interfaces.fs @@ -254,6 +254,10 @@ type IWriter = abstract MakeImportPath: string -> string abstract Write: string -> Async +/// Positional members take a 1-based `line` and a 0-based `col`, the same convention as the +/// ranges they return. Any column from the first character of an identifier up to the position +/// just after its last character resolves that identifier. For completions, `col` is the cursor +/// position and the text before it is the prefix to complete. type IFableManager = abstract Version: string diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index e82d1571a0..a316d6efef 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -372,7 +372,7 @@ type private SymbolImpl(symbol: FSharpSymbol) = let getSymbolAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> - match findLongIdents (col - 1, lineText) with + match findLongIdents (col, lineText) with | None -> None | Some(col, identIsland) -> checkFile.GetSymbolUseAtLocation(line, col, lineText, identIsland) @@ -395,7 +395,7 @@ let getUsesOfSymbolInFile (results: ParseAndCheckResults) (symbol: ISymbol) = let getDeclarationLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> - match findLongIdents (col - 1, lineText) with + match findLongIdents (col, lineText) with | None -> DeclarationLocation.NotFound "no identifier at location" | Some(col, identIsland) -> let (declarations: FindDeclResult) = @@ -504,7 +504,7 @@ let getSemanticClassification (results: ParseAndCheckResults) : SemanticClassifi let getToolTipAtLocation (results: ParseAndCheckResults) line col lineText = match results.CheckFileResultsOpt with | Some checkFile -> - match findLongIdents (col - 1, lineText) with + match findLongIdents (col, lineText) with | None -> [||] | Some(col, identIsland) -> let (ToolTipText els) = @@ -552,8 +552,8 @@ let private toCompletionItem (decl: DeclarationListItem) : Fable.Standalone.Comp let getCompletionsAtLocation (results: ParseAndCheckResults) (line: int) (col: int) lineText = match results.CheckFileResultsOpt with | Some checkFile -> - let ln, residue = findLongIdentsAndResidue (col - 1, lineText) - let longName = QuickParse.GetPartialLongNameEx(lineText, col - 1) + let ln, residue = findLongIdentsAndResidue (col, lineText) + let longName = QuickParse.GetPartialLongNameEx(lineText, col) let longName = { longName with diff --git a/src/fable-standalone/test/worker/run.mjs b/src/fable-standalone/test/worker/run.mjs index 666359e847..ce2416cadb 100644 --- a/src/fable-standalone/test/worker/run.mjs +++ b/src/fable-standalone/test/worker/run.mjs @@ -191,13 +191,15 @@ console.log(` diagnostics: ${diagnostics.join(", ")}`) const signatureOf = (tooltips) => (tooltips[0]?.Signature ?? []).map((t) => t.Text).join("") -post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000001", "Program.fs", 3, 29, greetLine]) +// Columns are 0-based: the first character of an identifier and the position just after its last +// character must both resolve it. +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000001", "Program.fs", 3, 34, greetLine]) const [, , sprintfTooltip] = await answer("FoundTooltip") const sprintfSignature = signatureOf(sprintfTooltip) if (!sprintfSignature.includes("Printf.StringFormat<")) failures.push(`tooltip has stray spaces: ${sprintfSignature}`) console.log(` tooltip: ${sprintfSignature}`) -post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000002", "Program.fs", 4, 15, runLine]) +post(["GetTooltipForFile", "00000000-0000-0000-0000-000000000002", "Program.fs", 4, 13, runLine]) const [, , greetTooltip] = await answer("FoundTooltip") const signature = greetTooltip[0]?.Signature ?? [] const greetSignature = signatureOf(greetTooltip) @@ -215,9 +217,9 @@ const declarationOf = async (id, line, col, lineText) => { const [, , location] = await answer("FoundDeclarationLocation") return location } -const greetDecl = await declarationOf("00000000-0000-0000-0000-000000000003", 4, 15, runLine) -const sprintfDecl = await declarationOf("00000000-0000-0000-0000-000000000004", 3, 29, greetLine) -const absDecl = await declarationOf("00000000-0000-0000-0000-000000000005", 5, 24, absLine) +const greetDecl = await declarationOf("00000000-0000-0000-0000-000000000003", 4, 13, runLine) +const sprintfDecl = await declarationOf("00000000-0000-0000-0000-000000000004", 3, 34, greetLine) +const absDecl = await declarationOf("00000000-0000-0000-0000-000000000005", 5, 22, absLine) if (greetDecl[0] !== "Found" || greetDecl[1].FileName !== "Program.fs" || greetDecl[1].StartLine !== 3) { failures.push(`greet should be found in Program.fs: ${JSON.stringify(greetDecl)}`) } @@ -230,7 +232,7 @@ if (absDecl[0] !== "External" || absDecl[2] !== "System.Math.Abs(System.Int32)") console.log(` declarations: greet in ${greetDecl[1]?.FileName}, sprintf in ${sprintfDecl[1]?.FileName}, Math.Abs in ${absDecl[1]}`) // Symbol uses: the definition of greet and its use must both come back, and nothing else. -post(["GetSymbolUsesForFile", "00000000-0000-0000-0000-000000000006", "Program.fs", 4, 15, runLine]) +post(["GetSymbolUsesForFile", "00000000-0000-0000-0000-000000000006", "Program.fs", 4, 13, runLine]) const [, , greetUses] = await answer("FoundSymbolUses") const usesText = greetUses.map((u) => `${u.Range.FileName}:${u.Range.StartLine}:${u.Range.StartColumn}-${u.Range.EndColumn}${u.IsDefinition ? " (definition)" : ""}`) const definitions = greetUses.filter((u) => u.IsDefinition) @@ -242,7 +244,7 @@ console.log(` symbol uses of greet: ${usesText.join(", ")}`) // Completions: the full item must come back, and the description of an item must be fetchable // afterwards by index. const completionsStarted = Date.now() -post(["GetCompletionsForFile", "00000000-0000-0000-0000-000000000007", "Program.fs", 6, 12, greLine]) +post(["GetCompletionsForFile", "00000000-0000-0000-0000-000000000007", "Program.fs", 6, 11, greLine]) const [, , greCompletions] = await answer("FoundCompletions") const completionsMs = Date.now() - completionsStarted const greetIndex = greCompletions.findIndex((c) => c.NameInList === "greet") From ada9d47b4169c79ae7ddc9bd0d23f1af625fbf4e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:27:43 +0200 Subject: [PATCH 190/322] chore: release multiple projects (#5061) chore: pass npm dist-tag when publishing prerelease packages --- src/Fable.Build/Publish.fs | 9 ++++++- src/Fable.Cli/CHANGELOG.md | 28 ++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 27 ++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 +-- src/fable-compiler-js/CHANGELOG.md | 35 +++++++++++++++++++++++- src/fable-compiler-js/package.json | 4 +-- src/fable-library-ts/CHANGELOG.md | 15 ++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 36 ++++++++++++++++++++++++- src/fable-standalone/package.json | 4 +-- 10 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/Fable.Build/Publish.fs b/src/Fable.Build/Publish.fs index 8ec96b561e..dcbfad026d 100644 --- a/src/Fable.Build/Publish.fs +++ b/src/Fable.Build/Publish.fs @@ -10,6 +10,7 @@ open EasyBuild.Tools.DotNet open EasyBuild.Tools.Changelog open EasyBuild.Tools.PackageJson open EasyBuild.Tools.Npm +open Semver let updateLibraryVersionInFableTransforms (compilerVersion: string) @@ -79,8 +80,14 @@ let publishNpm (projectDir: string) = PackageJson.replaceVersion (packageJsonFile, lastChangelogVersion) + let version = SemVersion.Parse(lastChangelogVersion, SemVersionStyles.Strict) + if PackageJson.needPublishing packageJsonFile then - Npm.publish (projectDir) + if version.IsPrerelease then + Npm.publish (projectDir, tag = version.PrereleaseIdentifiers.[0].Value) + else + Npm.publish (projectDir) + printfn $"Published!" else printfn $"Already up-to-date, skipping..." diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 0f9ff4fbe8..bf156464d2 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,32 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.22.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(all)* Forward `--targetprofile` for precompile (#5085) ([50129598](https://github.com/fable-compiler/Fable/commit/50129598a13d4ab7a6e50ae1be0d9aa9d3c68ef8)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Crack project references in parallel (#5079) ([d42aa6b9](https://github.com/fable-compiler/Fable/commit/d42aa6b926ac531aba7069aea408009f96584ae5)) +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) +* *(all)* Enable server GC and immediate JIT tiering (#5073) ([9bde2e41](https://github.com/fable-compiler/Fable/commit/9bde2e41d8483100ed14bbb37fa261896a6c88a4)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + ## 5.21.0 - 2026-10-12 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 9aa4b744e3..d2c1b7e079 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.21.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(all)* Forward `--targetprofile` for precompile (#5085) ([50129598](https://github.com/fable-compiler/Fable/commit/50129598a13d4ab7a6e50ae1be0d9aa9d3c68ef8)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Crack project references in parallel (#5079) ([d42aa6b9](https://github.com/fable-compiler/Fable/commit/d42aa6b926ac531aba7069aea408009f96584ae5)) +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + ## 5.20.0 - 2026-10-12 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index a009edd56c..81ee0f7ef2 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.21.0" + let VERSION = "5.22.0" [] - let JS_LIBRARY_VERSION = "2.8.1" + let JS_LIBRARY_VERSION = "2.9.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 769095d23d..1161ac2475 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0 - 2026-10-16 + +### 🏗️ Breaking changes + +* *(standalone)* Return tagged tooltips instead of text (#5088) ([6b995913](https://github.com/fable-compiler/Fable/commit/6b995913acbe98434f252b19b07182cc831a8a0f)) +* *(standalone)* Keep file name in declaration locations (#5089) ([b210461c](https://github.com/fable-compiler/Fable/commit/b210461c1651eaaa47e1027597c7545b6f2097fa)) +* *(standalone)* Return full completions with lazy docs (#5102) ([53849cc5](https://github.com/fable-compiler/Fable/commit/53849cc5a65f5de57dc65c76158b840e7012bc9c)) +* *(standalone)* Report error codes and severities (#5103) ([4ec83ff4](https://github.com/fable-compiler/Fable/commit/4ec83ff4e280db3075230e72b02b699a16f2bc76)) +* *(standalone)* Drop GetErrors, use Errors on results (#5111) ([ec72286d](https://github.com/fable-compiler/Fable/commit/ec72286d3528d1bbc1bede72df11deb3432040b4)) +* *(standalone)* Take 0-based columns in positional APIs (#5112) ([e33c5787](https://github.com/fable-compiler/Fable/commit/e33c57870454fea189a3eb33519f583a5003f8c3)) + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) +* *(standalone)* Expose symbol lookup and uses in file (#5101) ([9eff071c](https://github.com/fable-compiler/Fable/commit/9eff071c8e5cf418e4bf9af63c051eae121d4512)) +* *(standalone)* Expose lexer tokens and classification (#5110) ([e65d42c0](https://github.com/fable-compiler/Fable/commit/e65d42c03b8262b94e058354904f1126560443b4)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + ## 3.8.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 8d72702fc6..ec815a5b63 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "3.8.1", + "version": "4.0.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.1.1", - "@fable-org/fable-standalone": "^3.7.2" + "@fable-org/fable-standalone": "^4.0.0-beta.1" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 9c1fda5f0f..158e8745ae 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 updaters: - package.json: file: package.json @@ -15,6 +15,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.9.0 - 2026-10-16 + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) + +### 🐞 Bug Fixes + +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..e33c57870454fea189a3eb33519f583a5003f8c3) + ## 2.8.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index 870c0870a2..78c9d2ca7b 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.8.1", + "version": "2.9.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 0d2b565998..ae07356ec7 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 pre_release: beta include: - ../Fable.Transforms/ @@ -21,6 +21,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.1 - 2026-10-16 + +### 🏗️ Breaking changes + +* *(standalone)* Return tagged tooltips instead of text (#5088) ([6b995913](https://github.com/fable-compiler/Fable/commit/6b995913acbe98434f252b19b07182cc831a8a0f)) +* *(standalone)* Keep file name in declaration locations (#5089) ([b210461c](https://github.com/fable-compiler/Fable/commit/b210461c1651eaaa47e1027597c7545b6f2097fa)) +* *(standalone)* Return full completions with lazy docs (#5102) ([53849cc5](https://github.com/fable-compiler/Fable/commit/53849cc5a65f5de57dc65c76158b840e7012bc9c)) +* *(standalone)* Report error codes and severities (#5103) ([4ec83ff4](https://github.com/fable-compiler/Fable/commit/4ec83ff4e280db3075230e72b02b699a16f2bc76)) +* *(standalone)* Drop GetErrors, use Errors on results (#5111) ([ec72286d](https://github.com/fable-compiler/Fable/commit/ec72286d3528d1bbc1bede72df11deb3432040b4)) +* *(standalone)* Take 0-based columns in positional APIs (#5112) ([e33c5787](https://github.com/fable-compiler/Fable/commit/e33c57870454fea189a3eb33519f583a5003f8c3)) + +### 🚀 Features + +* *(js/ts/python/beam)* Support Async.TryCancelled (#5083) ([1643297d](https://github.com/fable-compiler/Fable/commit/1643297d9297fef536c8d10091d5b0d1fad77af7)) +* *(standalone)* Expose symbol lookup and uses in file (#5101) ([9eff071c](https://github.com/fable-compiler/Fable/commit/9eff071c8e5cf418e4bf9af63c051eae121d4512)) +* *(standalone)* Expose lexer tokens and classification (#5110) ([e65d42c0](https://github.com/fable-compiler/Fable/commit/e65d42c03b8262b94e058354904f1126560443b4)) + +### 🐞 Bug Fixes + +* *(all)* Recompile dependents on signature change in watch (#5084) ([87fc89f7](https://github.com/fable-compiler/Fable/commit/87fc89f75b6ad22fafec72b4a57415682b4059df)) +* *(beam)* Make lazy sequence wrappers process-portable (#5063) ([9e78a3b3](https://github.com/fable-compiler/Fable/commit/9e78a3b327ca78d01863bf19b18c543673633af7)) +* *(js/ts)* Avoid full array copy in Encoding.GetString (#5069) ([e6b72ec7](https://github.com/fable-compiler/Fable/commit/e6b72ec73be454bd3b3233167542a65fc6553c8a)) +* *(js/ts/python)* Preserve cancelled cleanup and generic unit callbacks (#5062) ([5dd6b497](https://github.com/fable-compiler/Fable/commit/5dd6b4979e79c8f7c4632cbfb5254d6cf0d2e8d2)) +* *(python)* Settle cancelled tasks awaited by async workflows (#5064) ([52186937](https://github.com/fable-compiler/Fable/commit/5218693738defc8a4502d28c206e391b241f9fd8)) +* *(python)* Preserve reusable replies and unit continuations (#5082) ([f19e2c56](https://github.com/fable-compiler/Fable/commit/f19e2c56566488dfeb48e71508bdb218aaa58582)) +* *(python)* Emit widening integer casts (#5087) ([07989b16](https://github.com/fable-compiler/Fable/commit/07989b16214c4fcf8e33318fba1dd3f77df77cbb)) +* *(rust)* Support inref struct unions (#5060) ([a3f27e0e](https://github.com/fable-compiler/Fable/commit/a3f27e0e8c149d6bfeb347ca4f4146ccfd8fb063)) + +### ⚡ Performance Improvements + +* Update inline expressions incrementally in watch mode (#5080) ([594d17b4](https://github.com/fable-compiler/Fable/commit/594d17b4b551ae33b5f2df5e4049611b9dd24bc2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..e33c57870454fea189a3eb33519f583a5003f8c3) + ## 3.7.2 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 00ec7619fe..f72e2ce724 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "3.7.2", - "fableVersion": "5.21.0", + "version": "4.0.0-beta.1", + "fableVersion": "5.22.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 20 Oct 2026 11:50:45 +0200 Subject: [PATCH 191/322] fix(standalone): warn and skip plugins instead of throwing (#5114) --- src/Fable.Transforms/State.fs | 29 ++++++++++++++++++++++++----- src/fable-standalone/src/Main.fs | 28 +++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 4ab442f112..496ddd55cf 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -14,7 +14,13 @@ type PluginRef = TypeFullName: string } -type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severity -> string -> unit) = +type Assemblies + ( + getPlugin: (PluginRef -> System.Type) option, + fsharpAssemblies: FSharpAssembly list, + addLog: Severity -> string -> unit + ) + = let assemblies = Dictionary() let coreAssemblies = Dictionary() let entities = ConcurrentDictionary() @@ -55,8 +61,23 @@ type Assemblies(getPlugin, fsharpAssemblies: FSharpAssembly list, addLog: Severi false if scanForPlugins then - for e in asm.Contents.Entities do - if e.IsAttributeType && FSharp2Fable.Util.inherits e "Fable.PluginAttribute" then + let pluginEntities = + asm.Contents.Entities + |> Seq.filter (fun e -> + e.IsAttributeType && FSharp2Fable.Util.inherits e "Fable.PluginAttribute" + ) + |> Seq.toList + + match getPlugin with + | None -> + if not pluginEntities.IsEmpty then + let names = pluginEntities |> List.map (fun e -> e.FullName) |> String.concat ", " + + addLog + Severity.Warning + $"Skipping Fable plugins from %s{asmName} because plugins cannot be loaded in this environment: %s{names}" + | Some getPlugin -> + for e in pluginEntities do try let plugin = getPlugin @@ -365,8 +386,6 @@ type Project let checknulls = projectOptions.OtherOptions |> Array.exists ((=) "--checknulls+") Compiler.SetCheckNullsUnsafe checknulls // set it one time only as early as possible - let getPlugin = defaultArg getPlugin (fun _ -> failwith "Plugins are not supported") - let assemblies = Assemblies(getPlugin, fsharpAssemblies, addLog) let implFilesMap = diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index a316d6efef..b69ae24298 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -37,6 +37,7 @@ let mapError (error: FSharpDiagnostic) = type ParseAndCheckResults ( project: Lazy, + projectLogs: ResizeArray, parseFileResultsOpt: FSharpParseFileResults option, checkFileResultsOpt: FSharpCheckFileResults option, checkProjectResults: FSharpCheckProjectResults, @@ -45,6 +46,12 @@ type ParseAndCheckResults = member _.GetProject() = project.Force() + + member _.TakeProjectLogs() = + let logs = projectLogs.ToArray() + projectLogs.Clear() + logs + member _.ParseFileResultsOpt = parseFileResultsOpt member _.CheckFileResultsOpt = checkFileResultsOpt member _.CheckProjectResults = checkProjectResults @@ -209,6 +216,7 @@ let makeProject (projectOptions: FSharpProjectOptions) (checkResults: FSharpCheckProjectResults) (precompiledInfo: IPrecompiledInfo option) + (logs: ResizeArray) = // let errors = com.GetFormattedLogs() |> Map.tryFind "error" // if errors.IsSome then failwith (errors.Value |> String.concat "\n") @@ -220,7 +228,7 @@ let makeProject else checkResults.AssemblyContents.ImplementationFiles - let addLog (severity: Fable.Severity) (msg: string) = () //TODO: log loading errors + let addLog (severity: Fable.Severity) (msg: string) = logs.Add(LogEntry.Make(severity, msg)) Project.From( projectOptions.ProjectFileName, @@ -243,8 +251,12 @@ let parseAndCheckProject let projectOptions = makeProjOptions projectFileName fileNames otherFSharpOptions - let project = lazy (makeProject projectOptions checkResults precompiledInfo) - ParseAndCheckResults(project, None, None, checkResults, otherFSharpOptions) + let projectLogs = ResizeArray() + + let project = + lazy (makeProject projectOptions checkResults precompiledInfo projectLogs) + + ParseAndCheckResults(project, projectLogs, None, None, checkResults, otherFSharpOptions) let parseAndCheckFileInProject (checker: InteractiveChecker) @@ -260,9 +272,12 @@ let parseAndCheckFileInProject let projectOptions = makeProjOptions projectFileName fileNames otherFSharpOptions - let project = lazy (makeProject projectOptions projectResults precompiledInfo) + let projectLogs = ResizeArray() + + let project = + lazy (makeProject projectOptions projectResults precompiledInfo projectLogs) - ParseAndCheckResults(project, Some results, Some checkResults, projectResults, otherFSharpOptions) + ParseAndCheckResults(project, projectLogs, Some results, Some checkResults, projectResults, otherFSharpOptions) type private FcsTextTag = FSharp.Compiler.Text.TextTag @@ -670,6 +685,9 @@ let compileToTargetAst (results: IParseAndCheckResults) fileName fableLibrary ty let com = makeCompiler fableLibrary typedArrays language results.OtherFSharpOptions project fileName + for log in res.TakeProjectLogs() do + (com :> Compiler).AddLog(log.Message, log.Severity, tag = log.Tag) + let fableAst = transformToFableAst com fableAst |> transformToTargetAst com From 5ee8bbb832d9677ad702104ded8e00c09619393f Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:52:36 +0800 Subject: [PATCH 192/322] feat(js/ts): add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) Co-authored-by: Claude Opus 5.5 --- src/Fable.Core/Fable.Core.Types.fs | 17 ++++++++++ src/Fable.Transforms/FSharp2Fable.Util.fs | 6 ++++ src/Fable.Transforms/FSharp2Fable.fs | 31 ++++++++++++++--- src/Fable.Transforms/Transforms.Util.fs | 3 ++ src/fable-metadata/lib/Fable.Core.dll | Bin 268800 -> 268800 bytes .../Compiler/CompilerMessagesTests.fs | 32 ++++++++++++++++++ tests/Js/Main/JsInteropTests.fs | 29 ++++++++++++++++ 7 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index 6d6e2ca30a..0ef2cd349e 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -85,6 +85,23 @@ type TypeScriptTaggedUnionAttribute(tagName: string, caseRules: CaseRules) = new(tagName: string) = TypeScriptTaggedUnionAttribute(tagName, CaseRules.LowerFirst) +/// +/// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches. +/// Use it when several cases share a tag value. +/// +/// +/// The test is a JS expression in Emit syntax, where $0 is the tested value: +/// +/// [<TypeScriptTaggedUnion("state")>] +/// type Terminal = +/// | [<CompiledName("done"); CaseTest("'value' in $0")>] Done of value: float +/// | [<CompiledName("done"); CaseTest("'error' in $0")>] DoneError of error: string +/// | Pending +/// +/// +type CaseTestAttribute(test: string) = + inherit Attribute() + /// /// Used in place of CompiledNameAttribute if the target is not a string. /// diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index c4de960e19..33613729e6 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -138,6 +138,12 @@ type FsUnionCase(uci: FSharpUnionCase) = | _ -> None ) + /// The macro of a CaseTestAttribute, an extra test for a TypeScriptTaggedUnion case + static member CaseTest(uci: FSharpUnionCase) = + uci.Attributes + |> Helpers.tryFindAttrib Atts.caseTest + |> Option.map (fun (att: FSharpAttribute) -> att.ConstructorArguments[0] |> snd |> string) + // static member HasNamedFields(uci: FSharpUnionCase) = // not (uci.Fields.Count = 1 && uci.Fields[0].Name = "Item") // true diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index b31c8b1a95..f14860ef00 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -576,8 +576,17 @@ let private transformUnionCaseTest = trampoline { let! unionExpr = transformExpr com ctx [] unionExpr + let unionPattern = getUnionPattern fsType unionCase - match getUnionPattern fsType unionCase with + do + match unionPattern, FsUnionCase.CaseTest unionCase with + | TypeScriptTaggedUnion _, _ + | _, None -> () + | _, Some _ -> + $"CaseTest attribute on '%s{unionCase.Name}' is ignored: it is only supported on TypeScriptTaggedUnion cases" + |> addWarning com ctx.InlinePath r + + match unionPattern with | ErasedUnionCase -> return "Cannot test erased union cases" |> addErrorAndReturnNull com ctx.InlinePath r | ErasedUnion(tdef, genArgs, rule) -> @@ -627,9 +636,23 @@ let private transformUnionCaseTest "CompileValue attribute is not supported in TypeScript" |> addErrorAndReturnNull com ctx.InlinePath r | _ -> - let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) - - return makeEqOp r getTag value BinaryEqual + let makeTagTest unionExpr = + let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) + makeEqOp r getTag value BinaryEqual + + match FsUnionCase.CaseTest unionCase with + | Some macro -> + // The union value is tested twice, so bind it first if it is not a plain value + let makeTest unionExpr = + let caseTest = emitExpr r Fable.Boolean [ unionExpr ] macro + makeLogOp r (makeTagTest unionExpr) caseTest LogicalAnd + + if canHaveSideEffects com unionExpr then + let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type + return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) + else + return makeTest unionExpr + | None -> return makeTagTest unionExpr | OptionUnion _ -> let kind = diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 5f0d7ae2b4..bbe89065d1 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -34,6 +34,9 @@ module Atts = [] let compiledValue = "Fable.Core.CompiledValueAttribute" // typeof.FullName + [] + let caseTest = "Fable.Core.CaseTestAttribute" // typeof.FullName + [] let entryPoint = "Microsoft.FSharp.Core.EntryPointAttribute" // typeof.FullName diff --git a/src/fable-metadata/lib/Fable.Core.dll b/src/fable-metadata/lib/Fable.Core.dll index ad480fca01b4b94d5671f136ba4a22eddb8e4a1d..990080732443d670eba3589c03c22fce09a429b2 100644 GIT binary patch literal 268800 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%0D^+tu}Xs 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 z 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 Assert.Is.success |> ignore + + testCase "CaseTest outside a TypeScriptTaggedUnion emits warning" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Id = + | [] Name of string + | [] Slug of string + +let isName (id: Id) = match id with Name _ -> true | Slug _ -> false +""" + compile source + |> Assert.Exists.warningWith "is ignored: it is only supported on TypeScriptTaggedUnion cases" + |> ignore + + testCase "CaseTest in a TypeScriptTaggedUnion does not emit warning" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string + +let isError (t: Terminal) = match t with DoneError _ -> true | Done _ -> false +""" + compile source + |> Assert.Is.strictSuccess + |> ignore ] diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 35c01030c1..d8a914b13f 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -391,6 +391,13 @@ module TaggedUnion = type FooGeneric<'a> = | Bar of baz: 'a + [] + type SharedTag = + | [] Done of value: float + | [] DoneError of error: string + | [] DoneFailed of ok: bool + | Pending + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -1090,6 +1097,28 @@ let tests = let fooGeneric = getFooGeneric 10 equal (TaggedUnion.FooGeneric.Bar 10) fooGeneric + + testCase "Case testing with TS tagged unions whose cases share a tag works" <| fun () -> + let describe = function + | TaggedUnion.SharedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.SharedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.SharedTag.DoneFailed _ -> "failed" + | TaggedUnion.SharedTag.Pending -> "pending" + TaggedUnion.SharedTag.Done 1.5 |> describe |> equal "done: 1.5" + TaggedUnion.SharedTag.DoneError "oops" |> describe |> equal "error: oops" + TaggedUnion.SharedTag.DoneFailed false |> describe |> equal "failed" + TaggedUnion.SharedTag.Pending |> describe |> equal "pending" + !!{| state = "done"; error = "raw" |} |> describe |> equal "error: raw" + (TaggedUnion.SharedTag.DoneError "oops").IsDoneError |> equal true + (TaggedUnion.SharedTag.Done 1.5).IsDoneError |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag evaluates the value once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + TaggedUnion.SharedTag.DoneError "oops" + (next ()).IsDoneError |> equal true + calls |> equal 1 #endif testCase "Pattern matching with StringEnum works" <| fun () -> From 3e84fd5448a1c8b294f56998b82789899dba223e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:09:18 +0200 Subject: [PATCH 193/322] chore: release multiple projects (#5115) --- src/Fable.Cli/CHANGELOG.md | 14 +++++++++++++- src/Fable.Compiler/CHANGELOG.md | 14 +++++++++++++- src/Fable.Core/CHANGELOG.md | 10 +++++++++- src/Fable.Transforms/Global/Compiler.fs | 2 +- src/fable-compiler-js/CHANGELOG.md | 14 +++++++++++++- src/fable-compiler-js/package.json | 6 +++--- src/fable-metadata/CHANGELOG.md | 10 +++++++++- src/fable-metadata/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 14 +++++++++++++- src/fable-standalone/package.json | 4 ++-- 10 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index bf156464d2..ad8b736213 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.23.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 5.22.0 - 2026-10-16 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index d2c1b7e079..7a62375a71 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.22.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 5.21.0 - 2026-10-16 ### 🚀 Features diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index d4886e19f5..e8d98f5165 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 148f989e1b37b3f6560c3dda615b6a3a8efffcb8 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.5.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/148f989e1b37b3f6560c3dda615b6a3a8efffcb8..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 5.4.0 - 2026-10-12 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 81ee0f7ef2..d1e384ddd6 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,7 +2,7 @@ namespace Fable module Literals = [] - let VERSION = "5.22.0" + let VERSION = "5.23.0" [] let JS_LIBRARY_VERSION = "2.9.0" diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 1161ac2475..eb47a9e68c 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.1.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 4.0.0 - 2026-10-16 ### 🏗️ Breaking changes diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index ec815a5b63..31ac1b46c7 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "4.0.0", + "version": "4.1.0", "main": "index.js", "bin": { "fable": "index.js" @@ -24,8 +24,8 @@ }, "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { - "@fable-org/fable-metadata": "^2.1.1", - "@fable-org/fable-standalone": "^4.0.0-beta.1" + "@fable-org/fable-metadata": "^2.2.0", + "@fable-org/fable-standalone": "^4.0.0-beta.2-beta.1" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-metadata/CHANGELOG.md b/src/fable-metadata/CHANGELOG.md index 7de1806b18..4681dfd1d4 100644 --- a/src/fable-metadata/CHANGELOG.md +++ b/src/fable-metadata/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: a8661874d60694f0faad235adb7f760fed6f72a5 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f updaters: - package.json: file: package.json @@ -15,6 +15,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.2.0 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/a8661874d60694f0faad235adb7f760fed6f72a5..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 2.1.1 - 2026-10-12 ### 🐞 Bug Fixes diff --git a/src/fable-metadata/package.json b/src/fable-metadata/package.json index 49ead91e6f..3e9c96c643 100644 --- a/src/fable-metadata/package.json +++ b/src/fable-metadata/package.json @@ -1,7 +1,7 @@ { "name": "@fable-org/fable-metadata", "private": false, - "version": "2.1.1", + "version": "2.2.0", "description": "Assemblies needed to compile F# projects with Fable", "main": "index.js", "type": "module", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index ae07356ec7..78ef6561de 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 +last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f pre_release: beta include: - ../Fable.Transforms/ @@ -21,6 +21,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.2 - 2026-10-21 + +### 🚀 Features + +* *(js/ts)* Add CaseTest attribute for TypeScriptTaggedUnion cases that share a tag (#5076) ([5ee8bbb8](https://github.com/fable-compiler/Fable/commit/5ee8bbb832d9677ad702104ded8e00c09619393f)) + +### 🐞 Bug Fixes + +* *(standalone)* Warn and skip plugins instead of throwing (#5114) ([021ab65c](https://github.com/fable-compiler/Fable/commit/021ab65c6d3adfe57cffb9f5ba6f595b034ac8bd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..5ee8bbb832d9677ad702104ded8e00c09619393f) + ## 4.0.0-beta.1 - 2026-10-16 ### 🏗️ Breaking changes diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index f72e2ce724..bfb4678383 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "4.0.0-beta.1", - "fableVersion": "5.22.0", + "version": "4.0.0-beta.2", + "fableVersion": "5.23.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From fca4c408a64e33c58293f12869975e5b61bcb96a Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Tue, 20 Oct 2026 10:59:19 -0400 Subject: [PATCH 194/322] fix(js/ts): round decimals below 0.1 correctly A decimal built from a number or a string with a nonzero value whose magnitude is below 0.1, such as `decimal -0.0836` or `Decimal.Parse "-0.0836"`, kept the zeros after the decimal point at the start of its digits. Its stored exponent therefore differed from the normalized exponent of the copy that round() rounds, so Math.Floor, Math.Ceiling and Math.Round rounded at the wrong decimal place: Math.Floor(decimal -0.0836) returned -0.1 instead of -1, and Math.Round(decimal 0.0836, 2) returned 0.084 instead of 0.08. GetHashCode also differed from that of an equal decimal produced by arithmetic, which broke HashSet and Dictionary lookups. Strip those leading zeros in parse() when the value has a nonzero digit. This gives these values the normalized form that copies and arithmetic results already have. Zero keeps them, to preserve its scale (0.00). Co-Authored-By: Claude Opus 5.5 --- src/fable-library-ts/lib/big.js | 5 +++++ tests/Js/Main/ArithmeticTests.fs | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index e2bb014635..3131132ede 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -194,6 +194,11 @@ function parse(x, n) { } x = round(x, Big.DP + 1, Big.RM); + + // Remove the leading zeros kept after the decimal point (e.g. 0.0836), so + // that x.e is the exponent of the first nonzero digit, as round() expects. + // Zero keeps them, to preserve its scale (e.g. 0.00). + if (hasNonzeroDigit(x)) normalize(x); return x; } diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index c369b71c52..e504d71cf7 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -377,6 +377,32 @@ let tests = Math.Floor 11.25M |> equal 11.M Math.Floor -11.25M |> equal -12.M + testCase "Decimal rounding works for values below 0.1" <| fun () -> + Math.Floor (decimal -0.0836) |> equal -1.M + Math.Floor (decimal (sin (-5. * Math.PI / 180.))) |> equal -1.M + Math.Floor (decimal "-0.0836") |> equal -1.M + Math.Floor (decimal "-0.0000123") |> equal -1.M + Math.Floor -0.0836M |> equal -1.M + Math.Floor (decimal 0.0836) |> equal 0.M + Math.Ceiling (decimal 0.0836) |> equal 1.M + Math.Ceiling (decimal "0.0000123") |> equal 1.M + Math.Ceiling (decimal -0.0836) |> equal 0.M + Math.Round (decimal 0.06) |> equal 0.M + Math.Round (decimal 0.0836, 2) |> equal 0.08M + Math.Round (decimal -0.0836, 3) |> equal -0.084M + Math.Truncate (decimal -0.0836) |> equal 0.M + Math.Floor (0.M - 0.0836M) |> equal -1.M + + testCase "Decimal values below 0.1 hash the same however they are built" <| fun () -> + (decimal -0.0836).GetHashCode() |> equal ((0.M - 0.0836M).GetHashCode()) + (decimal "0.05").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + let set = Collections.Generic.HashSet() + set.Add(decimal -0.0836) |> equal true + set.Add(0.M - 0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[decimal "0.05"] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + testCase "Decimal pown works" <| fun () -> pown 2.2M 3 |> equal 10.648M From 3076b45acacf39b8c99ec57025560ad355ab74db Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 11:16:12 +0200 Subject: [PATCH 195/322] fix(js/ts): keep decimal zero scale without leading zeros --- src/fable-library-ts/lib/big.js | 70 ++++++++++---------------------- tests/Js/Main/ArithmeticTests.fs | 20 +++++++++ 2 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 3131132ede..43152c0f02 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -95,7 +95,6 @@ function _Big_() { x.s = n.s; x.e = n.e; x.c = n.c.slice(); - normalize(x); } else { if (typeof n !== 'string') { if (Big.strict === true) { @@ -124,15 +123,6 @@ function _Big_() { return Big; } -function normalize(x) { - // x = round(x, DP, 0); - if (x.c.length > 1 && !x.c[0]) { - let i = x.c.findIndex(x => x); - x.c = x.c.slice(i); - x.e = x.e - i; - } -} - /* * Parse the number or string value passed to a Big constructor. * @@ -167,17 +157,14 @@ function parse(x, n) { nl = n.length; - // Determine leading zeros before decimal point. - for (i = 0; i < e && i < nl && n.charAt(i) == '0';) ++i; - - // original version (ignores decimal point). - // // Determine leading zeros. - // for (i = 0; i < nl && n.charAt(i) == '0';) ++i; + // Determine leading zeros. + for (i = 0; i < nl && n.charAt(i) == '0';) ++i; if (i == nl) { - // Zero. - x.c = [x.e = 0]; + // Zero, keeping the scale: 0.00 is c = [0], e = -2. + x.c = [0]; + x.e = nl > e ? e - nl : 0; } else { x.e = e - i - 1; @@ -193,12 +180,8 @@ function parse(x, n) { // for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); } - x = round(x, Big.DP + 1, Big.RM); - - // Remove the leading zeros kept after the decimal point (e.g. 0.0836), so - // that x.e is the exponent of the first nonzero digit, as round() expects. - // Zero keeps them, to preserve its scale (e.g. 0.00). - if (hasNonzeroDigit(x)) normalize(x); + // At most 29 significant digits and 28 decimal places, rounded half to even as Decimal.Parse. + x = round(x, Math.min(Big.DP + 1, x.e + 1 + Big.DP), 2); return x; } @@ -223,21 +206,14 @@ function round(x, sd, rm, more) { more = rm === 3 && (more || !!xc[0]) || sd === 0 && ( rm === 1 && xc[0] >= 5 || - rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc[1] !== UNDEFINED)) + rm === 2 && (xc[0] > 5 || xc[0] === 5 && (more || xc.some((d, i) => i > 0 && d))) ); xc.length = 1; - if (more) { - - // 1, 0.1, 0.01, 0.001, 0.0001 etc. - x.e = x.e - sd + 1; - xc[0] = 1; - } else { - - // Zero. - xc[0] = x.e = 0; - } + // 1, 0.1, 0.01, 0.001, 0.0001 etc., or zero at the same place. + x.e = x.e - sd + 1; + xc[0] = more ? 1 : 0; } else if (sd < xc.length) { // xc[sd] is the digit after the digit that may be rounded up. @@ -245,7 +221,7 @@ function round(x, sd, rm, more) { more = rm === 1 && xc[sd] >= 5 || rm === 2 && (xc[sd] > 5 || xc[sd] === 5 && - (more || xc[sd + 1] !== UNDEFINED || xc[sd - 1] & 1)) || + (more || xc.some((d, i) => i > sd && d) || xc[sd - 1] & 1)) || rm === 3 && (more || !isZero); // Remove any digits after the required precision. @@ -272,11 +248,6 @@ function round(x, sd, rm, more) { } -function hasNonzeroDigit(x) { - return x.c.some(d => d !== 0); -} - - /* * Return a string representing the value of Big x in normal or exponential notation. * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. @@ -288,6 +259,7 @@ function stringify(x, doExponential, isNonzero) { // Exponential notation? if (doExponential) { + if (!x.c[0]) e = 0; s = s.charAt(0) + (n > 1 ? '.' + s.slice(1) : '') + (e < 0 ? 'e' : 'e+') + e; // Normal notation. @@ -942,7 +914,7 @@ P.times = P.mul = function (y) { */ P.toExponential = function (dp, rm) { var x = this, - n = hasNonzeroDigit(x); + n = x.c[0]; if (dp !== UNDEFINED) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { @@ -952,7 +924,7 @@ P.toExponential = function (dp, rm) { for (; x.c.length < dp;) x.c.push(0); } - return stringify(x, true, n); + return stringify(x, true, !!n); }; @@ -973,14 +945,13 @@ P.toFixed = function (dp, rm) { if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { throw Error(INVALID_DP); } - x = new x.constructor(x); - x = round(x, dp + x.e + 1, rm); + x = round(new x.constructor(x), dp + x.e + 1, rm); // x.e may have changed if the value is rounded up. for (dp = dp + x.e + 1; x.c.length < dp;) x.c.push(0); } - return stringify(x, false, hasNonzeroDigit(x)); + return stringify(x, false, !!x.c[0]); }; @@ -993,7 +964,7 @@ P.toFixed = function (dp, rm) { P.toJSON = P.toString = function () { var x = this, Big = x.constructor; - return stringify(x, x.e <= Big.NE || x.e >= Big.PE, hasNonzeroDigit(x)); + return stringify(x, x.e <= Big.NE || x.e >= Big.PE, !!x.c[0]); }; @@ -1021,17 +992,18 @@ P.toNumber = function () { P.toPrecision = function (sd, rm) { var x = this, Big = x.constructor, - n = hasNonzeroDigit(x); + n = x.c[0]; if (sd !== UNDEFINED) { if (sd !== ~~sd || sd < 1 || sd > MAX_DP) { throw Error(INVALID + 'precision'); } x = round(new Big(x), sd, rm); + if (!x.c[0]) x.e = 0; for (; x.c.length < sd;) x.c.push(0); } - return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, n); + return stringify(x, sd <= x.e || x.e <= Big.NE || x.e >= Big.PE, !!n); }; diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index e504d71cf7..ae154e6f17 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -403,6 +403,26 @@ let tests = dict.[decimal "0.05"] <- "a" dict.ContainsKey(0.1M / 2.M) |> equal true + testCase "Decimal parsing rounds to 28 places and keeps the scale of zero" <| fun () -> + string (decimal "0.0000000000000000000000000000004") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000005") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000015") |> equal "0.0000000000000000000000000002" + string (decimal "0.000000000000000000000000000012") |> equal "0.0000000000000000000000000000" + Decimal.GetBits(decimal "0.0000000000000000000000000000004") |> equal [| 0; 0; 0; 1835008 |] + Decimal.GetBits(decimal "0.00") |> equal [| 0; 0; 0; 131072 |] + string (-(0.00M)) |> equal "0.00" + string (abs 0.00M) |> equal "0.00" + string (Math.Round(0.004M, 2)) |> equal "0.00" + (0.00M).ToString("E3", System.Globalization.CultureInfo.InvariantCulture) |> equal "0.000E+000" + (decimal "0.00").GetHashCode() |> equal (Decimal(0, 0, 0, false, 2uy).GetHashCode()) + + testCase "Decimal rounding is half to even when a trailing zero follows the 5" <| fun () -> + Math.Round 2.50M |> equal 2.M + Math.Round 0.50M |> equal 0.M + Math.Round(1.250M, 1) |> equal 1.2M + Math.Round(0.050M, 1) |> equal 0.0M + Math.Round(0.0050M, 2) |> equal 0.00M + testCase "Decimal pown works" <| fun () -> pown 2.2M 3 |> equal 10.648M From 32a69e0d00f567c9a838307e9ed51b3b1d519473 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 11:21:53 +0200 Subject: [PATCH 196/322] fix(js/ts): hash decimals by value in collections --- src/Fable.Transforms/Replacements.fs | 2 +- src/fable-library-ts/Decimal.ts | 10 +++++++++- tests/Js/Main/ArithmeticTests.fs | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index aec37196c9..29755864fc 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -617,7 +617,7 @@ let structuralHash (com: ICompiler) r (arg: Expr) = | Char | String | Builtin BclGuid -> "stringHash" - | Number(Decimal, _) -> "fastStructuralHash" + | Number(Decimal, _) -> "safeHash" | Number(BigIntegers _, _) -> "bigintHash" | Number(Numbers _, _) -> "numberHash" | Builtin BclTimeSpan diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index c564838b28..07c81de552 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -7,7 +7,15 @@ import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } fr import * as bigInt from "./BigInt.ts"; Decimal.prototype.GetHashCode = function () { - return combineHashCodes([this.s, this.e].concat(this.c)) + const digits: number[] = this.c; + if (digits[0] === 0) { + return combineHashCodes([1, 0, 0]); + } + let end = digits.length; + while (digits[end - 1] === 0) { + end--; + } + return combineHashCodes([this.s, this.e].concat(digits.slice(0, end))); } Decimal.prototype.Equals = function (x: Decimal) { diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index ae154e6f17..9112949c7d 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -403,6 +403,19 @@ let tests = dict.[decimal "0.05"] <- "a" dict.ContainsKey(0.1M / 2.M) |> equal true + testCase "Decimal hash ignores trailing zeros and the sign of zero" <| fun () -> + (decimal "1.0").GetHashCode() |> equal ((decimal "1").GetHashCode()) + (decimal "0.050").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + (decimal "0.00").GetHashCode() |> equal ((decimal "0").GetHashCode()) + (-(0.M)).GetHashCode() |> equal ((0.M).GetHashCode()) + let set = Collections.Generic.HashSet([ 1.M ]) + set.Contains 1.0M |> equal true + set.Add 0.M |> equal true + set.Add(Math.Truncate -0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[0.050M] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + testCase "Decimal parsing rounds to 28 places and keeps the scale of zero" <| fun () -> string (decimal "0.0000000000000000000000000000004") |> equal "0.0000000000000000000000000000" string (decimal "0.00000000000000000000000000005") |> equal "0.0000000000000000000000000000" From b9d46840ebb69c3318c45168a2020e7cedc4cab2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:58:44 +0200 Subject: [PATCH 197/322] chore: Bump the all-pip group with 3 updates (#5121) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 6 ++-- uv.lock | 88 +++++++++++++++++++++++++------------------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bab49efc12..2eb5700318 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.17.0,<0.18", + "ruff>=0.17.1,<0.18", "maturin>=1.15.0,<2", - "dunamai>=1.26.2,<2", + "dunamai>=1.26.3,<2", "pydantic>=2.14.1", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.86", + "ty>=0.0.89", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index e9193f5bc0..0cd456fa7a 100644 --- a/uv.lock +++ b/uv.lock @@ -31,14 +31,14 @@ wheels = [ [[package]] name = "dunamai" -version = "1.26.2" +version = "1.26.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/18/020d3b27a10450ddb11429f637404e8ea67ecf4d9fd999d4f1d553f25506/dunamai-1.26.2.tar.gz", hash = "sha256:84ea45eddf9bb4b40df7610b1b22a03137365e6257dbf9d7b72128fdccca564c", size = 46536, upload-time = "2026-08-02T03:32:50.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/62/edf5352d8ce1371129ccfe971e6de3a1ce4dc7f9bdc80390fb79856b78d4/dunamai-1.26.3.tar.gz", hash = "sha256:fe2f732186710ee5048044fdbef6c76c6afbb9ee7de4f6d00cf34256e9ec2e7f", size = 46820, upload-time = "2026-10-14T20:13:30.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/31/2aaabe7d03f395c7b6d955f09a4d1440a2c49920abea42fde88cacc2ef97/dunamai-1.26.2-py3-none-any.whl", hash = "sha256:4234be3a90c3ec13ecf75d94a2248068e5a3018e8112892fcd9f3a1f287c9c32", size = 27478, upload-time = "2026-08-02T03:32:49.348Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/e3be26f6301535ab4a3a940b09e55615c6286195b07fec9aa2cf8b842563/dunamai-1.26.3-py3-none-any.whl", hash = "sha256:cc486963f21ad5b5b20d95424909ff5029990241eea3e2272865e1542c5fd19f", size = 27628, upload-time = "2026-10-14T20:13:29.974Z" }, ] [[package]] @@ -63,13 +63,13 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "attrs", specifier = ">=26.1.0" }, - { name = "dunamai", specifier = ">=1.26.2,<2" }, + { name = "dunamai", specifier = ">=1.26.3,<2" }, { name = "maturin", specifier = ">=1.15.0,<2" }, { name = "pydantic", specifier = ">=2.14.1" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.17.0,<0.18" }, - { name = "ty", specifier = ">=0.0.86" }, + { name = "ruff", specifier = ">=0.17.1,<0.18" }, + { name = "ty", specifier = ">=0.0.89" }, ] [[package]] @@ -290,52 +290,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.0" +version = "0.17.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/a7/70debb024dfacda67b8e560cc7511f52b34b9348a8cc8c1ec23036dcd51d/ruff-0.17.0.tar.gz", hash = "sha256:5cd03240d8208a557c2a9655a5cb07ebe36aa6bb35065f97d48c1f6adef5a322", size = 5320554, upload-time = "2026-10-09T19:47:29.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/f8/ee5ab9da6089eae2a33e6008b01deb1eda19992c1c8e10661e98cee1640f/ruff-0.17.0-py3-none-linux_armv6l.whl", hash = "sha256:0e271826af9a20d18c6cfae8c51e82959167c24859686ddd3eb9a7f0842ce81e", size = 10148907, upload-time = "2026-10-09T19:46:38.695Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d9/2f81fb5a9d580afbb11b1c8ff915233a11f2a1b27405d7991f183c5e1976/ruff-0.17.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5f0ca4a40f81403689c04f12966e22f44e329ae362072d8f1587b7bda87f603b", size = 10224208, upload-time = "2026-10-09T19:46:41.711Z" }, - { url = "https://files.pythonhosted.org/packages/a7/20/643f3c8f75594f937b2bf74801241c56a2e2b8e139d24dff8b66b28cdd7f/ruff-0.17.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cbf7149e0927dc3295d5d64679a4765576eef71b00782b2ae969ef82274d6bb9", size = 9273324, upload-time = "2026-10-09T19:46:44.323Z" }, - { url = "https://files.pythonhosted.org/packages/ec/91/627700b233d367736cb274f1bd0b47d1f2b12f68878192812bd875adadc3/ruff-0.17.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ee90156522998c3037059d8f66885c8adeeaf7643bdce2caceee196ecd23e0", size = 9953190, upload-time = "2026-10-09T19:46:47.155Z" }, - { url = "https://files.pythonhosted.org/packages/cd/92/91f7b5ed39490f89d6cbf56e1f543c383667a725efa8e2c2dee0f01f5591/ruff-0.17.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d8e4a002a94cd9d0dc48b51dc69d807a172b5b9bf2b668e656424dc5b55ead1", size = 9961746, upload-time = "2026-10-09T19:46:50.098Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/7310f9fc63ce11ff6394edbd5e85433dfb0e14c9fbf6ccc97f1538491bc7/ruff-0.17.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0b8a60c06a218c337e1161638d34757f83449243e2db161483ddf948e53ad14", size = 10625483, upload-time = "2026-10-09T19:46:53.379Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8d/97443f0dca4a03a0bc7629fd396fd494a1cb6666121e38c5075acb217d8f/ruff-0.17.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a330178bdffc4205dbf3bda11d93e059e388fd6546f8cdd304501a9160363c0d", size = 11350129, upload-time = "2026-10-09T19:46:56.486Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0a/c525efd9777be4b6b012e6969a3012648468e7e6c4b3e5b46af69f46e8eb/ruff-0.17.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bb08489e234876fa2da67ae3ea938e9a2156da80293e0e4365abd6973d98329", size = 10906223, upload-time = "2026-10-09T19:47:00.263Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3c/4a01195d93420cad1175bedad13a515dc8a56f95a6e39789b92e582682f5/ruff-0.17.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7c133e82d55b5f15897b2a442d72c0cb4a0c886c46801ce3c247150b60c", size = 10386314, upload-time = "2026-10-09T19:47:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/c7/72/1a3951665485a921f6375f91e754a1854d5a645d41acc3642668064ff64d/ruff-0.17.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:db4f74c533403ab70fe4007873f6ae0c9f94a8b03158cf48d78788e47cdbe399", size = 10565464, upload-time = "2026-10-09T19:47:05.831Z" }, - { url = "https://files.pythonhosted.org/packages/90/8c/539b4d8c082f57e18db8ae2be85a460d77861c79dcd5798e32b536a6a06f/ruff-0.17.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3d8cc360e666d1914e47b0777c6906d70cf18891a55532bd0a16844195d70859", size = 10079468, upload-time = "2026-10-09T19:47:08.617Z" }, - { url = "https://files.pythonhosted.org/packages/e0/b8/84286966db79434e8c26b585b0a0f6897cb3ab1c51a4aa4df10c28488b62/ruff-0.17.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d66de796b726c4801e05fa99a2a8d7a780e107be222486c304ab61765561e866", size = 9958556, upload-time = "2026-10-09T19:47:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/27b6eed27b83fcdd5bfa0d52b83231e29094754374698da404d094487ae3/ruff-0.17.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c3f268baf004aea944f040623327119527ea231af15f7fb7890e82cea0679589", size = 10352173, upload-time = "2026-10-09T19:47:14.188Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/955399fd13044cd827862044117d784a59e3196f6cce7424908ac9a7f914/ruff-0.17.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:864b6c1acb6b0bccf94b5a3938a1531fd09aaca5e5659a2e7bf0f3cf2a685540", size = 10791761, upload-time = "2026-10-09T19:47:16.931Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bf/024e01e1f6aec87768696725e648ed5b438941341eea8f8100beb681961f/ruff-0.17.0-py3-none-win32.whl", hash = "sha256:5e50aa5b84decd9fe5b0bb0e6f71c3b592f1767ed09faa4b7207d933961e35cd", size = 9904112, upload-time = "2026-10-09T19:47:19.75Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/1ee73df41dcc8d1cdb686ee4bc46ea29704ea175feb6b95c78420f631ab8/ruff-0.17.0-py3-none-win_amd64.whl", hash = "sha256:8ab76bcda86dfd28e13776cb5de3c7bcdcf1ae3d37ed761113d1a5a415dc134c", size = 10115585, upload-time = "2026-10-09T19:47:22.698Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/eb4f0ccc844aece56963e8578df9c95d4c00f580547d52329d4035d3af18/ruff-0.17.0-py3-none-win_arm64.whl", hash = "sha256:c154c73ff43f9854395e24cac507af13078962e53d2b511605058d22af1fdb88", size = 9872035, upload-time = "2026-10-09T19:47:26.306Z" }, + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, ] [[package]] name = "ty" -version = "0.0.86" +version = "0.0.89" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/fa/9b35728cc3c04d64f434c656836fa3c5f6e384a7813e48e63245510d5c69/ty-0.0.86.tar.gz", hash = "sha256:6edcaf52e207d653873d5f495c1a8470075b232030714c981f07f0a1075bc343", size = 7911723, upload-time = "2026-10-09T22:20:57.83Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz", hash = "sha256:bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4", size = 7978241, upload-time = "2026-10-16T10:35:05.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/27/dd422283667ce10d60af57edc3ccab078084282ee304a5c22b5ed79390c6/ty-0.0.86-py3-none-linux_armv6l.whl", hash = "sha256:305c238df5dedf97f1b65e52d2c446a41d11551291e37054858c00c5627bf15b", size = 14159390, upload-time = "2026-10-09T22:20:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/57/e3/838ea547c177983976103d3a54b346afc22d0ff97943ab58c7b7aff1f753/ty-0.0.86-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27a8a7cf26a1cd17d93586eb20aadbcc535fa6ad4b535813ee8fc52560c31efd", size = 13684426, upload-time = "2026-10-09T22:20:19.939Z" }, - { url = "https://files.pythonhosted.org/packages/bc/62/dfde84ffab702545f866350675b16f8df6555d3c41123c25e7a509fe857e/ty-0.0.86-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40e5fc602af41b0ea89dd1d19fe8da7e75322e6e0db95ea23340a858b409ac2b", size = 12845457, upload-time = "2026-10-09T22:20:21.979Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/7e0e7ffc8b3961f5f74893ba9b98433b922d3f1ae6621d1e3e3e20663e1d/ty-0.0.86-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4b525ce4feb60aa2dd6becae4c470daea889149ba9dc7699ffbcf41448f69a9", size = 13656310, upload-time = "2026-10-09T22:20:24.544Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c8/977594e024d154f0e064b48a6f7e39a0e1e55a524349dba22146f01e02db/ty-0.0.86-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a000ddcc0b99bd1ab8c8277630711061a36ae83395780fdb935bad540ebe7103", size = 13887036, upload-time = "2026-10-09T22:20:26.844Z" }, - { url = "https://files.pythonhosted.org/packages/3a/75/67e8e7bf89a6be7535768380cb20569b460b89cb09d5c82ce27411782786/ty-0.0.86-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3848f3cab206e9798e5a9f7c4ce8a80297b0a88ba9aa8a091360a298e97424", size = 14805103, upload-time = "2026-10-09T22:20:29.274Z" }, - { url = "https://files.pythonhosted.org/packages/a1/14/a7f3b88cc76156feaf5d48601bd5da96a116f846ad0abc526df03a735bcc/ty-0.0.86-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:087bad6ae8fa7248a16c01f5646c121dccaacbaf320ff544eed4e8c5c1fd7555", size = 15203931, upload-time = "2026-10-09T22:20:31.736Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/42f6ad5d4bb7091d70a0e96d7d436d6bc9a490210a928727dde5d883ed26/ty-0.0.86-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7f2721746c9ee64626c69da9245faedc0f375024bf41458e45eb7b28fe5f63", size = 15094434, upload-time = "2026-10-09T22:20:34.269Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/211781d3c8de5fa88437412b7a8c0d7a632596033cff51d5004b08e94e1c/ty-0.0.86-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5c18501c54874149d05e5199211146876afeaeee2af234042e56dccc9d5812a", size = 14436812, upload-time = "2026-10-09T22:20:36.795Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/518cc7ef2586384345be675d470e0087591810304b5d994d0a2194c7d68e/ty-0.0.86-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0a48967c8adba6b12035243f225a0585b0bc5f0bf940a4829876bc455ebf3d2f", size = 14671116, upload-time = "2026-10-09T22:20:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/7a/18/a820553ad3206d0cdfad5866dd64939c7e3be73e891d88c73f7fd3acb5d6/ty-0.0.86-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:561a4eae98f6ff05395e8d7be8151fdb328eeaead1ac63dc721ca891623f7924", size = 13586361, upload-time = "2026-10-09T22:20:41.444Z" }, - { url = "https://files.pythonhosted.org/packages/22/58/f1700176e6ae4d0a3d25e447734c60ca464d6e76ed2e8a32920ebd41e523/ty-0.0.86-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccad053ce880a1f8c0850d9dd1f277f3ee559a98bded3f93472268376ceccc2d", size = 13900289, upload-time = "2026-10-09T22:20:43.955Z" }, - { url = "https://files.pythonhosted.org/packages/59/86/2f0e3c9a055f8606911a80311763a089784b66af22dd80fd3290e3b167c6/ty-0.0.86-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7c16d22857bad1da56b147bf53ef0ed2252d7677dc5e7ee51c757b2e52ab6351", size = 14235785, upload-time = "2026-10-09T22:20:46.401Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1d/72fa2347b641143b0440296144a65f424fe047302cb13af059988f1de7a4/ty-0.0.86-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d863f8f190e96878ed1900a8a6d5093f37761c0c47409e2bdb3d9370e37471e1", size = 14559840, upload-time = "2026-10-09T22:20:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/47/4a/a798be4aee4ea0bb427c22f157945d0ac41310f4d6e058b3758dfb859451/ty-0.0.86-py3-none-win32.whl", hash = "sha256:1ac3d8fe9efae04fbf1407583ed4118e0364a45b49a020d62b202846695a2ba1", size = 13133227, upload-time = "2026-10-09T22:20:51.034Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a0/c9d585fa1c5bf4ed2727f576f1542bc13e268f036451a79d057c249fc26f/ty-0.0.86-py3-none-win_amd64.whl", hash = "sha256:6fe4116fb1a7ad3c4f7804d9241dc81187717fbd9f5fed5b50e2752a20e17a67", size = 13665391, upload-time = "2026-10-09T22:20:53.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/7e/d9811358bce1e54ac1b93bd44f4c45eba5e2e92672c5410248776c54319e/ty-0.0.86-py3-none-win_arm64.whl", hash = "sha256:0df36f494c5d07f68ebec3edb74fc719c4c959db7061c2018f1dff2dfe62150b", size = 13237981, upload-time = "2026-10-09T22:20:55.586Z" }, + { url = "https://files.pythonhosted.org/packages/c5/84/e199d9a3dc3c94c804ddd9e94b63dbe543ae7f39329574963b6a6a95872f/ty-0.0.89-py3-none-linux_armv6l.whl", hash = "sha256:739b087e8873e177ffda99ff8a3600a4a7704d06523a8e88c4493021eee40b8a", size = 14254879, upload-time = "2026-10-16T10:34:24.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/5afe09bf5c83c4e3a9e5913386a460e240a7029f2ae5fc96df5b43563a91/ty-0.0.89-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:47ebc98480c8a301955e18a8bdd67ef07090071ad47349b3254f325d3d5ab4e7", size = 13790417, upload-time = "2026-10-16T10:34:27.672Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f9/88648f0ba9400dfd642c1e8dc0aeecb87945b121f3ff51f3902883c016d6/ty-0.0.89-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0631291af5c850a15007c0ffb906d813966466e4ec12173c44977c0c5a2d51d", size = 12856900, upload-time = "2026-10-16T10:34:29.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/baae2b407101c4cb02f19216123900c147af8fd87cdb1ed7eba139472734/ty-0.0.89-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d768300ae16274ce656006e9199869ad68684b74400d963e1c82deddf3a22ad", size = 13653330, upload-time = "2026-10-16T10:34:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/293516c2e10a77134cfc40d22c3fc08edd8c5eadae9c75531ec8a9263348/ty-0.0.89-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dd4c57ba58bab8f6c816b2c254db52b58834b5dbf8268287382f7a2f9735c8f", size = 13986215, upload-time = "2026-10-16T10:34:34.506Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c3/da911c8fc22d2664692432570619576b25c7cd0a586401cab08987763094/ty-0.0.89-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1af774c24117d366b69142d4f35edee518617f8b926d13560d0334cbe5aa3f85", size = 14905008, upload-time = "2026-10-16T10:34:36.632Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/aed8d920b9bbf7b582e9fc0305acb2f2c748ede788e7dba9d6952e939c78/ty-0.0.89-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8147811b9b3bc2dc9338cd01c7893ac931fc3fae7aace169047ecbd627e6628", size = 15278217, upload-time = "2026-10-16T10:34:39.144Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/c6b4320f08fca44ffe279e5ddeb3eae12869c6341f596931accdefea1182/ty-0.0.89-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d5f178c91d50fb62461b5e579028b7d2e92872e1d8f02e96ace26464c2277a6", size = 15206289, upload-time = "2026-10-16T10:34:41.626Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc", size = 14510395, upload-time = "2026-10-16T10:34:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/f0/32/5f9ce2377a77ad9ff36d8996edf86b1f069c1e8716e37c7d6d668402b110/ty-0.0.89-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a70ab1a402746d7837407dc169e461fdf90945bc1eef0d3c0f1be543490060bf", size = 14767366, upload-time = "2026-10-16T10:34:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/2b28c71d6ebd97b5bb1378f1806598c5dd90278f7b99a431dd3879dddcfc/ty-0.0.89-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b55c28a49bd2676eac9b3540a1dcc4cc053e4671c65dcec9fdad3dfe55fb148c", size = 13670284, upload-time = "2026-10-16T10:34:48.225Z" }, + { url = "https://files.pythonhosted.org/packages/34/cc/68b8ebf98c9bc582fddc3d2b93f6e0f53719d1ddee1c5037dfa2e1cb8467/ty-0.0.89-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c9f22edeb8176f8ab189337e8cbd8f92b66744534a2886df9e252b52ade991c", size = 13997891, upload-time = "2026-10-16T10:34:50.556Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4d/a1d61e06092fd5726f48a8711bd32eac6af132308eb222a0027caba4f42d/ty-0.0.89-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c186185cad57a1469b851096b51345de7b625e353c1f4acd338402daed22fa7", size = 14326247, upload-time = "2026-10-16T10:34:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/466d3762e330504a2c114dbbd0f0383229f9652dccd56277dde008bfe0da/ty-0.0.89-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5e38302ac218d2404758241093b5542ed0345d4f569096589f1810034244e998", size = 14661527, upload-time = "2026-10-16T10:34:55.555Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d5/dfd7ed5e4af472b20a11ef651da4bca55f9dc8be485cd152337f94caddde/ty-0.0.89-py3-none-win32.whl", hash = "sha256:0146637ed2321717280bfe4e4b20a18813b741da280e8039d7df2bf22b0251f1", size = 13224078, upload-time = "2026-10-16T10:34:57.735Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/1ee663c422e3a2d910c5cc27e7d84bd7709a3df0aca28a713843c6c3fa5d/ty-0.0.89-py3-none-win_amd64.whl", hash = "sha256:9fc56322a1aff29a19088d2025f3c42a6d25a7035668048bb908c9f8b66f480d", size = 13727433, upload-time = "2026-10-16T10:35:00.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/93/af8f7250920823ca6d582bcf0abf803fb92c8b99415ba7f40f2304102c6f/ty-0.0.89-py3-none-win_arm64.whl", hash = "sha256:1d4967c8f7bfd67c9708115b41d7049197c5d20c76fb2e136fe78b0437ada05b", size = 13287101, upload-time = "2026-10-16T10:35:02.918Z" }, ] [[package]] From 51ff4c4c38e24b2cdcd63808f1e5ebcd83b4d261 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:00:49 +0200 Subject: [PATCH 198/322] chore: Bump markdown-it from 14.3.2 to 14.3.3 (#5124) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 231f9c3679..0a3c8b67d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1545,9 +1545,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", - "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "version": "14.3.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", + "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", "dev": true, "funding": [ { From c49ce397ec008471d23cb151783a03e51e611802 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:01:47 +0200 Subject: [PATCH 199/322] chore: Bump rollup from 4.64.5 to 4.64.9 in the all-npm group (#5118) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 208 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 105 insertions(+), 105 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a3c8b67d5..31f85d180d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.5", + "rollup": "^4.64.9", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", - "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.9.tgz", + "integrity": "sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", - "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.9.tgz", + "integrity": "sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", - "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.9.tgz", + "integrity": "sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", - "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.9.tgz", + "integrity": "sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", - "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.9.tgz", + "integrity": "sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", - "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.9.tgz", + "integrity": "sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", - "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.9.tgz", + "integrity": "sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", - "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.9.tgz", + "integrity": "sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", - "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.9.tgz", + "integrity": "sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", - "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.9.tgz", + "integrity": "sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", - "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.9.tgz", + "integrity": "sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", - "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.9.tgz", + "integrity": "sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", - "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.9.tgz", + "integrity": "sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", - "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.9.tgz", + "integrity": "sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", - "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.9.tgz", + "integrity": "sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", - "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.9.tgz", + "integrity": "sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", - "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.9.tgz", + "integrity": "sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", - "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.9.tgz", + "integrity": "sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", - "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.9.tgz", + "integrity": "sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", - "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.9.tgz", + "integrity": "sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", - "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.9.tgz", + "integrity": "sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", - "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.9.tgz", + "integrity": "sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", - "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.9.tgz", + "integrity": "sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", - "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.9.tgz", + "integrity": "sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", - "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.9.tgz", + "integrity": "sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==", "cpu": [ "x64" ], @@ -2330,9 +2330,9 @@ } }, "node_modules/rollup": { - "version": "4.64.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", - "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", + "version": "4.64.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.9.tgz", + "integrity": "sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2346,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.64.5", - "@rollup/rollup-android-arm64": "4.64.5", - "@rollup/rollup-darwin-arm64": "4.64.5", - "@rollup/rollup-darwin-x64": "4.64.5", - "@rollup/rollup-freebsd-arm64": "4.64.5", - "@rollup/rollup-freebsd-x64": "4.64.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", - "@rollup/rollup-linux-arm-musleabihf": "4.64.5", - "@rollup/rollup-linux-arm64-gnu": "4.64.5", - "@rollup/rollup-linux-arm64-musl": "4.64.5", - "@rollup/rollup-linux-loong64-gnu": "4.64.5", - "@rollup/rollup-linux-loong64-musl": "4.64.5", - "@rollup/rollup-linux-ppc64-gnu": "4.64.5", - "@rollup/rollup-linux-ppc64-musl": "4.64.5", - "@rollup/rollup-linux-riscv64-gnu": "4.64.5", - "@rollup/rollup-linux-riscv64-musl": "4.64.5", - "@rollup/rollup-linux-s390x-gnu": "4.64.5", - "@rollup/rollup-linux-x64-gnu": "4.64.5", - "@rollup/rollup-linux-x64-musl": "4.64.5", - "@rollup/rollup-openbsd-x64": "4.64.5", - "@rollup/rollup-openharmony-arm64": "4.64.5", - "@rollup/rollup-win32-arm64-msvc": "4.64.5", - "@rollup/rollup-win32-ia32-msvc": "4.64.5", - "@rollup/rollup-win32-x64-gnu": "4.64.5", - "@rollup/rollup-win32-x64-msvc": "4.64.5", + "@rollup/rollup-android-arm-eabi": "4.64.9", + "@rollup/rollup-android-arm64": "4.64.9", + "@rollup/rollup-darwin-arm64": "4.64.9", + "@rollup/rollup-darwin-x64": "4.64.9", + "@rollup/rollup-freebsd-arm64": "4.64.9", + "@rollup/rollup-freebsd-x64": "4.64.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.9", + "@rollup/rollup-linux-arm-musleabihf": "4.64.9", + "@rollup/rollup-linux-arm64-gnu": "4.64.9", + "@rollup/rollup-linux-arm64-musl": "4.64.9", + "@rollup/rollup-linux-loong64-gnu": "4.64.9", + "@rollup/rollup-linux-loong64-musl": "4.64.9", + "@rollup/rollup-linux-ppc64-gnu": "4.64.9", + "@rollup/rollup-linux-ppc64-musl": "4.64.9", + "@rollup/rollup-linux-riscv64-gnu": "4.64.9", + "@rollup/rollup-linux-riscv64-musl": "4.64.9", + "@rollup/rollup-linux-s390x-gnu": "4.64.9", + "@rollup/rollup-linux-x64-gnu": "4.64.9", + "@rollup/rollup-linux-x64-musl": "4.64.9", + "@rollup/rollup-openbsd-x64": "4.64.9", + "@rollup/rollup-openharmony-arm64": "4.64.9", + "@rollup/rollup-win32-arm64-msvc": "4.64.9", + "@rollup/rollup-win32-ia32-msvc": "4.64.9", + "@rollup/rollup-win32-x64-gnu": "4.64.9", + "@rollup/rollup-win32-x64-msvc": "4.64.9", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index f84c42b833..c5043aa79d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.5", + "rollup": "^4.64.9", "terser": "^5.51.2", "typescript": "^7.0.2" }, From bdf0a6c8cb56151a5431b5e3703f7ee165b26dba Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 21 Oct 2026 13:01:28 +0200 Subject: [PATCH 200/322] fix(js/ts): keep .NET decimal scale through arithmetic (#5126) --- src/fable-library-ts/lib/big.js | 140 +++++++++++++++++++++++-------- tests/Js/Main/ArithmeticTests.fs | 84 +++++++++++++++++++ 2 files changed, 189 insertions(+), 35 deletions(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 43152c0f02..3ead2d1554 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -180,9 +180,7 @@ function parse(x, n) { // for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++); } - // At most 29 significant digits and 28 decimal places, rounded half to even as Decimal.Parse. - x = round(x, Math.min(Big.DP + 1, x.e + 1 + Big.DP), 2); - return x; + return fit(x); } @@ -240,14 +238,70 @@ function round(x, sd, rm, more) { } } - // Remove trailing zeros. - for (sd = xc.length; !xc[--sd];) xc.pop(); } return x; } +// Largest System.Decimal mantissa, 2^96 - 1. +var MAX_MANTISSA = '79228162514264337593543950335'.split('').map(Number), + OVERFLOW = 'Value was either too large or too small for a Decimal.'; + + +/* + * Return the number of decimal places of Big x. + */ +function scaleOf(x) { + return x.c.length - x.e - 1; +} + + +/* + * Pad Big x with trailing zeros up to dp decimal places. + */ +function padScale(x, dp) { + if (!x.c[0]) { + x.e = Math.min(x.e, -dp); + } else { + for (; scaleOf(x) < dp;) x.c.push(0); + } + return x; +} + + +/* + * Return true if the mantissa c is larger than MAX_MANTISSA. + */ +function exceedsMax(c) { + if (c.length !== MAX_MANTISSA.length) return c.length > MAX_MANTISSA.length; + for (var i = 0; i < c.length; i++) { + if (c[i] !== MAX_MANTISSA[i]) return c[i] > MAX_MANTISSA[i]; + } + return false; +} + + +/* + * Round Big x half to even so that it fits a System.Decimal: at most Big.DP decimal places + * and a mantissa no larger than MAX_MANTISSA. Throws when no decimal place is left to drop. + */ +function fit(x) { + var dp = Math.min(scaleOf(x), Big.DP, Big.DP - x.e); + + if (dp < 0) throw Error(OVERFLOW); + if (dp < scaleOf(x)) round(x, x.e + 1 + dp, 2); + + if (exceedsMax(x.c)) { + if (!scaleOf(x)) throw Error(OVERFLOW); + round(x, x.c.length - 1, 2); + } + if (x.e > Big.DP) throw Error(OVERFLOW); + + return x; +} + + /* * Return a string representing the value of Big x in normal or exponential notation. * Handles P.toExponential, P.toFixed, P.toJSON, P.toPrecision, P.toString and P.valueOf. @@ -347,28 +401,40 @@ P.cmp = function (y) { * if necessary, to a maximum of Big.DP decimal places using rounding mode Big.RM. */ P.div = function (y) { - var - Big = this.constructor, - x = new Big(this), - y = new Big(y), - a = x.c, // dividend - b = y.c, // divisor - k = x.s == y.s ? 1 : -1, + var Big = this.constructor, dp = Big.DP; if (dp !== ~~dp || dp < 0 || dp > MAX_DP) { throw Error(INVALID_DP); } + return divide(new Big(this), new Big(y), dp, 2); +}; + + +/* + * Return Big x divided by Big y to dp decimal places using rounding mode rm. + * Both are modified. + */ +function divide(x, y, dp, rm) { + var + a = x.c, // dividend + b = y.c, // divisor + k = x.s == y.s ? 1 : -1; + // Divisor is zero? if (!b[0]) { throw Error(DIV_BY_ZERO); } + // Scale of the exact quotient is at least the scale of x less the scale of y. + var minScale = Math.max(0, scaleOf(x) - scaleOf(y)); + // Dividend is 0? Return +-0. if (!a[0]) { y.s = k; - y.c = [y.e = 0]; + y.c = [0]; + y.e = -minScale; return y; } @@ -448,11 +514,20 @@ P.div = function (y) { p--; } + var exact = r[0] === UNDEFINED; + // Round? - if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED); + if (qi > p) round(q, p, rm, !exact); + fit(q); + + if (!q.c[0]) { + q.e = 0; + } else if (exact) { + for (; scaleOf(q) > minScale && !q.c[q.c.length - 1];) q.c.pop(); + } return q; -}; +} /* @@ -522,6 +597,7 @@ P.minus = P.sub = function (y) { // Either zero? if (!xc[0] || !yc[0]) { + var dp = Math.max(scaleOf(x), scaleOf(y)); if (yc[0]) { y.s = -b; } else if (xc[0]) { @@ -529,7 +605,7 @@ P.minus = P.sub = function (y) { } else { y.s = 1; } - return y; + return padScale(y, dp); } // Determine which is the bigger number. Prepend zeros to equalise exponents. @@ -584,8 +660,8 @@ P.minus = P.sub = function (y) { xc[j] -= yc[j]; } - // Remove trailing zeros. - for (; xc[--b] === 0;) xc.pop(); + // The result keeps the larger scale. + var dp = xc.length - ye - 1; // Remove leading zeros and adjust exponent accordingly. for (; xc[0] === 0;) { @@ -599,13 +675,14 @@ P.minus = P.sub = function (y) { y.s = 1; // Result must be zero. - xc = [ye = 0]; + xc = [0]; + ye = -dp; } y.c = xc; y.e = ye; - return y; + return fit(y); }; @@ -631,12 +708,7 @@ P.mod = function (y) { if (ygtx) return new Big(x); - a = Big.DP; - b = Big.RM; - Big.DP = Big.RM = 0; - x = x.div(y); - Big.DP = a; - Big.RM = b; + x = divide(x, new Big(y), 0, 0); return this.minus(x.times(y)); }; @@ -664,6 +736,7 @@ P.plus = P.add = function (y) { // Either zero? if (!xc[0] || !yc[0]) { + var dp = Math.max(scaleOf(x), scaleOf(y)); if (!yc[0]) { if (xc[0]) { y = new Big(x); @@ -671,7 +744,7 @@ P.plus = P.add = function (y) { y.s = x.s; } } - return y; + return padScale(y, dp); } xc = xc.slice(); @@ -711,13 +784,10 @@ P.plus = P.add = function (y) { ++ye; } - // Remove trailing zeros. - for (e = xc.length; xc[--e] === 0;) xc.pop(); - y.c = xc; y.e = ye; - return y; + return fit(y); }; @@ -853,7 +923,9 @@ P.times = P.mul = function (y) { // Return signed 0 if either 0. if (!xc[0] || !yc[0]) { - y.c = [y.e = 0]; + var dp = scaleOf(x) + scaleOf(y); + y.c = [0]; + y.e = -dp; return y; } @@ -897,11 +969,9 @@ P.times = P.mul = function (y) { if (b) ++y.e; else c.shift(); - // Remove trailing zeros. - for (i = c.length; !c[--i];) c.pop(); y.c = c; - return y; + return fit(y); }; diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 9112949c7d..b41fe57232 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -429,6 +429,90 @@ let tests = (0.00M).ToString("E3", System.Globalization.CultureInfo.InvariantCulture) |> equal "0.000E+000" (decimal "0.00").GetHashCode() |> equal (Decimal(0, 0, 0, false, 2uy).GetHashCode()) + testCase "Decimal addition and subtraction keep the larger scale" <| fun () -> + string (1.50M + 0.5M) |> equal "2.00" + string (1.5M + 0.50M) |> equal "2.00" + string (0.1M + 0.2M) |> equal "0.3" + string (1M + 1M) |> equal "2" + string (0.00M + 0M) |> equal "0.00" + string (0.00M + 1.5M) |> equal "1.50" + string (1.0M + (-1.0M)) |> equal "0.0" + string (0.05M - 0.05M) |> equal "0.00" + string (1.00M - 0.50M) |> equal "0.50" + string (100.5M - 0.5M) |> equal "100.0" + string (1.5M - 0.00M) |> equal "1.50" + + testCase "Decimal multiplication adds the scales up to 28 places" <| fun () -> + string (2.0M * 3.0M) |> equal "6.00" + string (1.5M * 2M) |> equal "3.0" + string (0.5M * 0.5M) |> equal "0.25" + string (1.50M * 1.0M) |> equal "1.500" + string (10M * 0.1M) |> equal "1.0" + string (1.10M * 1.10M) |> equal "1.2100" + string (0.0M * 5M) |> equal "0.0" + string (0M * 1.50M) |> equal "0.00" + string (pown 1.50M 2) |> equal "2.2500" + string (0.1234567890123456789012345678M * 0.1M) |> equal "0.0123456789012345678901234568" + string (0.0000000000000000000000000025M * 0.1M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000035M * 0.1M) |> equal "0.0000000000000000000000000004" + string (123456789012345678901234567.89M * 1.1M) |> equal "135802467913580246791358024.68" + + testCase "Decimal division keeps the scale of an exact quotient" <| fun () -> + string (1.00M / 2M) |> equal "0.50" + string (10M / 2M) |> equal "5" + string (1M / 4M) |> equal "0.25" + string (1.0M / 0.5M) |> equal "2" + string (2M / 1.0M) |> equal "2" + string (5.0M / 2.0M) |> equal "2.5" + string (1.000M / 8M) |> equal "0.125" + string (1.23M / 0.1M) |> equal "12.3" + string (0.0M / 5M) |> equal "0.0" + + testCase "Decimal division rounds an inexact quotient half to even to fit a decimal" <| fun () -> + string (1M / 3M) |> equal "0.3333333333333333333333333333" + string (2M / 3M) |> equal "0.6666666666666666666666666667" + string (10M / 3M) |> equal "3.3333333333333333333333333333" + string (100M / 3M) |> equal "33.333333333333333333333333333" + string (1M / 30000M) |> equal "0.0000333333333333333333333333" + string (1M / 7M) |> equal "0.1428571428571428571428571429" + string (80M / 9M) |> equal "8.888888888888888888888888889" + string (1000000000000000000000000000M / 3M) |> equal "333333333333333333333333333.33" + string (10000000000000000000000000001M / 2M) |> equal "5000000000000000000000000000.5" + string (0.0000000000000000000000000005M / 2M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000007M / 2M) |> equal "0.0000000000000000000000000004" + string (0.0000000000000000000000000001M / 2M) |> equal "0" + + testCase "Decimal remainder keeps the larger scale" <| fun () -> + string (5.0M % 2M) |> equal "1.0" + string (5M % 2.0M) |> equal "1.0" + string (5.5M % 2M) |> equal "1.5" + string (7.00M % 2.5M) |> equal "2.00" + string (-5.0M % 2M) |> equal "-1.0" + string (1.5M % 2.00M) |> equal "1.5" + + testCase "Decimal arithmetic and parsing throw on overflow" <| fun () -> + (fun () -> Decimal.MaxValue + 1M) |> throwsError "" + (fun () -> Decimal.MaxValue + 0.5M) |> throwsError "" + string (Decimal.MaxValue + 0.4M) |> equal "79228162514264337593543950335" + string ((Decimal.MaxValue - 1M) + 1M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MinValue - 1M) |> throwsError "" + (fun () -> Decimal.MaxValue * 1.1M) |> throwsError "" + string (Decimal.MaxValue * 1.0M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MaxValue / 0.5M) |> throwsError "" + (fun () -> decimal "1e30") |> throwsError "" + (fun () -> decimal "79228162514264337593543950336") |> throwsError "" + (fun () -> decimal "79228162514264337593543950335.5") |> throwsError "" + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + (fun () -> decimal 1e30) |> throwsError "" + + testCase "Decimal rounding keeps the requested scale" <| fun () -> + string (Math.Round(1.20001M, 2)) |> equal "1.20" + string (Math.Round(1.2M, 3)) |> equal "1.2" + string (Math.Round(1.99M, 1)) |> equal "2.0" + string (Math.Floor 1.50M) |> equal "1" + string (Math.Ceiling 1.50M) |> equal "2" + string (Math.Truncate 1.50M) |> equal "1" + testCase "Decimal rounding is half to even when a trailing zero follows the 5" <| fun () -> Math.Round 2.50M |> equal 2.M Math.Round 0.50M |> equal 0.M From 798df10179576a28f602fad58e204881792a0aef Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 21 Oct 2026 13:38:37 +0200 Subject: [PATCH 201/322] fix(js/ts): warn on type test against Pojo class (#5128) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 6 +++++- .../Integration/Compiler/CompilerMessagesTests.fs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index f3a630f375..0fc37fa99b 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -531,7 +531,10 @@ module Reflection = |> libCall com ctx range "Util" "isException" [] | _ -> match com.GetEntity(entRef) with - | Patterns.Try (Util.tryFindAnyEntAttribute [ Atts.stringEnum; Atts.erase; Atts.tsTaggedUnion ]) (att, _) as ent -> + | Patterns.Try (Util.tryFindAnyEntAttribute [ Atts.stringEnum + Atts.erase + Atts.tsTaggedUnion + Atts.pojoDefinedByConsArgs ]) (att, _) as ent -> match att with | Atts.stringEnum -> jsTypeof "string" expr | Atts.erase when ent.IsFSharpUnion -> @@ -541,6 +544,7 @@ module Reflection = | cases when cases |> List.forall (fun c -> List.isEmpty c.UnionCaseFields) -> jsTypeof "string" expr | _ -> warnAndEvalToFalse (ent.FullName + " (erased)") + | Atts.pojoDefinedByConsArgs -> warnAndEvalToFalse (ent.FullName + " (pojo)") | _ -> warnAndEvalToFalse (ent.FullName + " (erased)") | Patterns.Try (tryJsConstructorFor ActualConsRef com ctx) cons -> diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 8a876ea99d..e686023c12 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -315,4 +315,19 @@ let isError (t: Terminal) = match t with DoneError _ -> true | Done _ -> false compile source |> Assert.Is.strictSuccess |> ignore + + testCase "Type test against a Pojo class emits warning" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Options(name: string) = + member val name = name + +let isOptions (o: obj) = match o with :? Options -> true | _ -> false +""" + compile source + |> Assert.Exists.warningWith "Cannot type test (evals to false): Program.Options (pojo)" + |> ignore ] From c9feb339fb8a4e98bebf17b3facdc42ecd22d628 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 13:04:58 +0200 Subject: [PATCH 202/322] fix(js/ts): accept decimal integers up to 2^96 --- src/fable-library-ts/lib/big.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/fable-library-ts/lib/big.js b/src/fable-library-ts/lib/big.js index 3ead2d1554..d2cbd9d6d2 100644 --- a/src/fable-library-ts/lib/big.js +++ b/src/fable-library-ts/lib/big.js @@ -271,12 +271,12 @@ function padScale(x, dp) { /* - * Return true if the mantissa c is larger than MAX_MANTISSA. + * Return true if the digits c, padded with zeros to the length of MAX_MANTISSA, exceed it. */ function exceedsMax(c) { - if (c.length !== MAX_MANTISSA.length) return c.length > MAX_MANTISSA.length; - for (var i = 0; i < c.length; i++) { - if (c[i] !== MAX_MANTISSA[i]) return c[i] > MAX_MANTISSA[i]; + for (var i = 0; i < MAX_MANTISSA.length; i++) { + var d = i < c.length ? c[i] : 0; + if (d !== MAX_MANTISSA[i]) return d > MAX_MANTISSA[i]; } return false; } @@ -287,16 +287,16 @@ function exceedsMax(c) { * and a mantissa no larger than MAX_MANTISSA. Throws when no decimal place is left to drop. */ function fit(x) { - var dp = Math.min(scaleOf(x), Big.DP, Big.DP - x.e); + var scale = scaleOf(x), + dp = Math.min(scale, Big.DP, Big.DP - x.e); - if (dp < 0) throw Error(OVERFLOW); - if (dp < scaleOf(x)) round(x, x.e + 1 + dp, 2); - - if (exceedsMax(x.c)) { - if (!scaleOf(x)) throw Error(OVERFLOW); - round(x, x.c.length - 1, 2); - } if (x.e > Big.DP) throw Error(OVERFLOW); + if (dp < scale) round(x, x.e + 1 + dp, 2); + if (x.e > Big.DP) throw Error(OVERFLOW); + + // Both the value and the mantissa itself must not exceed MAX_MANTISSA. + if (x.e === Big.DP && exceedsMax(x.c)) throw Error(OVERFLOW); + if (x.c.length === MAX_MANTISSA.length && exceedsMax(x.c)) round(x, x.c.length - 1, 2); return x; } From 6430cdacec34e6327434af649eb0c9427d65608c Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 21 Oct 2026 13:04:59 +0200 Subject: [PATCH 203/322] fix(js/ts): align decimal conversion, parsing and G format --- src/Fable.Transforms/Replacements.fs | 19 ++++- src/fable-library-ts/Decimal.ts | 106 +++++++++++++++++++++++++-- src/fable-library-ts/String.ts | 4 + src/fable-library-ts/lib/big.d.ts | 1 + tests/Js/Main/ArithmeticTests.fs | 78 ++++++++++++++++++++ 5 files changed, 197 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 29755864fc..adcc960ad5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -289,6 +289,8 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr = match kind with | Decimal -> args.Head | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toDecimal", targetType, args) + | Float32 -> Helper.LibCall(com, "Decimal", "fromFloat32", targetType, args, ?loc = r) + | Float64 -> Helper.LibCall(com, "Decimal", "fromFloat64", targetType, args, ?loc = r) | _ -> makeDecimalFromExpr com r targetType args.Head | _ -> addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" @@ -2696,7 +2698,9 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op [] let args = - if isFloatOrDecimal then + if kind = Decimal then + [ str; makeIntConst style ] @ outValue + elif isFloatOrDecimal then [ str ] @ outValue else [ str; makeIntConst style; makeBoolConst unsigned; makeIntConst bitsize ] @@ -2745,7 +2749,11 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let hexConst = int System.Globalization.NumberStyles.HexNumber let intConst = int System.Globalization.NumberStyles.Integer - if style <> hexConst && style <> intConst then + if + style <> hexConst + && style <> intConst + && i.DeclaringEntityFullName <> Types.decimal + then $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" |> addWarning com ctx.InlinePath r @@ -2773,7 +2781,12 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" |> addWarning com ctx.InlinePath r - let style = int System.Globalization.NumberStyles.Any + let style = + if i.DeclaringEntityFullName = Types.decimal then + int System.Globalization.NumberStyles.Number + else + int System.Globalization.NumberStyles.Any + parseCall meth str args style |> Some | "Pow", _ -> Helper.GlobalCall("Math", t, args, i.SignatureArgTypes, memb = "pow", ?loc = r) diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index 07c81de552..016398d222 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -57,7 +57,7 @@ export function equals(x: Decimal, y: Decimal) { export function abs(x: Decimal) { return x.abs(); } export function sign(x: Decimal): number { return x.lt(get_Zero) ? -1 : x.gt(get_Zero) ? 1 : 0; } -export function max(x: Decimal, y: Decimal): Decimal { return x.gt(y) ? x : y; } +export function max(x: Decimal, y: Decimal): Decimal { return x.lt(y) ? y : x; } export function min(x: Decimal, y: Decimal): Decimal { return x.lt(y) ? x : y; } export function maxMagnitude(x: Decimal, y: Decimal): Decimal { return abs(x).gt(abs(y)) ? x : y; } @@ -132,22 +132,81 @@ export function toString(x: Decimal) { return x.toString(); } -export function tryParse(str: string, defValue: FSharpRef): boolean { +const NumberStyles = { + AllowLeadingWhite: 1, + AllowTrailingWhite: 2, + AllowLeadingSign: 4, + AllowTrailingSign: 8, + AllowParentheses: 16, + AllowDecimalPoint: 32, + AllowThousands: 64, + AllowExponent: 128, + AllowHexSpecifier: 512, + Number: 111, +} as const; + +// The digits of a decimal string once whitespace, sign and parentheses are stripped +function digitsPattern(style: number): RegExp { + const integral = style & NumberStyles.AllowThousands ? "\\d+(?:,\\d+)*" : "\\d+"; + const point = style & NumberStyles.AllowDecimalPoint ? "(?:\\.\\d*)?" : ""; + const leadingPoint = style & NumberStyles.AllowDecimalPoint ? "|\\.\\d+" : ""; + const exponent = style & NumberStyles.AllowExponent ? "(?:[eE][+-]?\\d+)?" : ""; + return new RegExp(`^(?:${integral}${point}${leadingPoint})${exponent}$`); +} + +// Returns undefined when the string does not have the format allowed by the style +function parseWithStyle(str: string, style: number): Decimal | undefined { + if (style & NumberStyles.AllowHexSpecifier) { + throw new Exception("The number style AllowHexSpecifier is not supported on floating point data types."); + } + let s = str; + if (style & NumberStyles.AllowLeadingWhite) { + s = s.replace(/^\s+/, ""); + } + if (style & NumberStyles.AllowTrailingWhite) { + s = s.replace(/\s+$/, ""); + } + let negative = false; + if (style & NumberStyles.AllowParentheses && s.startsWith("(") && s.endsWith(")")) { + s = s.slice(1, -1); + negative = true; + } + if (style & NumberStyles.AllowLeadingSign && /^[+-]/.test(s)) { + negative = s[0] === "-" ? !negative : negative; + s = s.slice(1); + } else if (style & NumberStyles.AllowTrailingSign && /[+-]$/.test(s)) { + negative = s[s.length - 1] === "-" ? !negative : negative; + s = s.slice(0, -1); + } + if (!digitsPattern(style).test(s)) { + return undefined; + } + const d = new Decimal(s.replace(/,/g, "")); + if (negative) { + d.s = -1; + } + return d; +} + +export function tryParse(str: string, style: number, defValue: FSharpRef): boolean { try { - defValue.contents = new Decimal(str.trim()); + const d = parseWithStyle(str, style); + if (d === undefined) { + return false; + } + defValue.contents = d; return true; } catch { return false; } } -export function parse(str: string): Decimal { - const defValue = new FSharpRef(get_Zero); - if (tryParse(str, defValue)) { - return defValue.contents; - } else { +export function parse(str: string, style: number = NumberStyles.Number): Decimal { + const d = parseWithStyle(str, style); + if (d === undefined) { throw new Exception(`The input string ${str} was not in a correct format.`); } + return d; } export function toNumber(x: Decimal): number { @@ -228,6 +287,37 @@ function getInt32Bits(hexDigits: Uint8Array, offset: number) { return bits; } +const OVERFLOW = "Value was either too large or too small for a Decimal."; + +// The first double outside the decimal range +const MAX_DOUBLE = 2 ** 96; + +function fromFloat(n: number, significantDigits: number): Decimal { + if (!isFinite(n) || Math.abs(n) >= MAX_DOUBLE) { + throw new Exception(OVERFLOW); + } + const exact = Math.abs(n) >= 1e21 ? BigInt(n).toString() : n.toFixed(100); + const d = new Decimal(exact).prec(significantDigits, 2); + const digits: number[] = d.c; + let end = digits.length; + while (end > 1 && digits[end - 1] === 0) { + end--; + } + d.c = digits.slice(0, end); + if (d.c[0] === 0) { + d.e = 0; + } + return d; +} + +export function fromFloat64(n: number): Decimal { + return fromFloat(n, 15); +} + +export function fromFloat32(n: number): Decimal { + return fromFloat(n, 7); +} + export function fromIntArray(bits: ArrayLike) { return fromInts(bits[0], bits[1], bits[2], bits[3]); } diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 44ad183ec9..d230d12e3e 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -438,6 +438,10 @@ export function format(str: string | object, ...args: any[]) { } break; case "g": case "G": { + if (precision == null && typeof rep !== "number" && typeof rep !== "bigint") { + rep = String(rep); + break; + } rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); // Handle exponential notation: only trim trailing zeros from mantissa, not from exponent. // .NET G format guarantees an exponent of at least 2 digits with an explicit sign (e.g. "E-07"). diff --git a/src/fable-library-ts/lib/big.d.ts b/src/fable-library-ts/lib/big.d.ts index 521dc72ece..50c783eff4 100644 --- a/src/fable-library-ts/lib/big.d.ts +++ b/src/fable-library-ts/lib/big.d.ts @@ -202,6 +202,7 @@ export interface Big extends CustomNumeric { * @throws `!Big.RM!` if rm is invalid. */ round(dp?: number, rm?: RoundingMode): Big; + prec(sd: number, rm?: RoundingMode): Big; /** * Returns a Big number whose value is the square root of this Big number. * diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index b41fe57232..43833cf96d 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.Arithmetic open System +open System.Globalization open Util.Testing open Fable.Tests.Util @@ -505,6 +506,83 @@ let tests = string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" (fun () -> decimal 1e30) |> throwsError "" + testCase "Decimal from float rounds to 15 significant digits" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (decimal 0.1) |> equal "0.1" + string (decimal (0.1 + 0.2)) |> equal "0.3" + string (decimal 123456789.123456789) |> equal "123456789.123457" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + string (decimal 123456789012345.6789) |> equal "123456789012346" + string (decimal 123456789012345678.0) |> equal "123456789012346000" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal 1e28) |> equal "10000000000000000000000000000" + string (decimal 1e-10) |> equal "0.0000000001" + string (decimal 1.5e-28) |> equal "0.0000000000000000000000000002" + string (decimal 5e-29) |> equal "0" + string (decimal 1e-30) |> equal "0" + string (decimal -0.0) |> equal "0" + string (decimal 100000000000001.5) |> equal "100000000000002" + string (decimal 100000000000002.5) |> equal "100000000000002" + (fun () -> decimal 1e29) |> throwsError "" + (fun () -> decimal nan) |> throwsError "" + (fun () -> decimal infinity) |> throwsError "" + + testCase "Decimal from float32 rounds to 7 significant digits" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (decimal 1.1f) |> equal "1.1" + string (decimal (0.1f + 0.2f)) |> equal "0.3" + string (decimal 16777216.f) |> equal "16777220" + string (decimal 1.23456789f) |> equal "1.234568" + string (decimal 123456.789f) |> equal "123456.8" + string (decimal 1000001.5f) |> equal "1000002" + string (decimal 1000002.5f) |> equal "1000002" + string (decimal 1e-30f) |> equal "0" + (fun () -> decimal 3.4e38f) |> throwsError "" + + testCase "Decimal.Parse follows NumberStyles" <| fun () -> + let inv = CultureInfo.InvariantCulture + Decimal.Parse("+5", inv) |> equal 5M + Decimal.Parse(" 5 ", inv) |> equal 5M + Decimal.Parse("5-", inv) |> equal -5M + Decimal.Parse("1,234.5", inv) |> equal 1234.5M + Decimal.Parse("1,23,4", inv) |> equal 1234M + Decimal.Parse(".5", inv) |> equal 0.5M + Decimal.Parse("5.", inv) |> equal 5M + string (Decimal.Parse("-0.00", inv)) |> equal "0.00" + (fun () -> Decimal.Parse("1e3", inv)) |> throwsError "" + (fun () -> Decimal.Parse(",5", inv)) |> throwsError "" + (fun () -> Decimal.Parse("1,234.5,6", inv)) |> throwsError "" + (fun () -> Decimal.Parse("(5)", inv)) |> throwsError "" + (fun () -> Decimal.Parse("", inv)) |> throwsError "" + Decimal.Parse("1.5E+2", NumberStyles.Float, inv) |> equal 150M + string (Decimal.Parse("1.50e1", NumberStyles.Float, inv)) |> equal "15.0" + Decimal.Parse("1e3", NumberStyles.Any, inv) |> equal 1000M + Decimal.Parse("(5)", NumberStyles.Any, inv) |> equal -5M + (fun () -> Decimal.Parse("1,234", NumberStyles.Float, inv)) |> throwsError "" + (fun () -> Decimal.Parse("1e30", NumberStyles.Float, inv)) |> throwsError "" + let ok1, v1 = Decimal.TryParse("1,5", NumberStyles.Number, inv) + ok1 |> equal true + v1 |> equal 15M + let ok2, _ = Decimal.TryParse("1e3", NumberStyles.Number, inv) + ok2 |> equal false + + testCase "Decimal G format keeps the scale without a precision" <| fun () -> + let inv = CultureInfo.InvariantCulture + (1.10M).ToString("G", inv) |> equal "1.10" + (0.00M).ToString("G", inv) |> equal "0.00" + (1.10M).ToString("G5", inv) |> equal "1.1" + (123456M).ToString("G3", inv) |> equal "1.23E+05" + (0.0001234M).ToString("G3", inv) |> equal "0.000123" + (2.5M).ToString("G1", inv) |> equal "3" + (1.5M).ToString("G1", inv) |> equal "2" + + testCase "Decimal max returns the first of two equal values" <| fun () -> + let inv = CultureInfo.InvariantCulture + string (max 1.0M 1.00M) |> equal "1.0" + string (min 1.0M 1.00M) |> equal "1.00" + string (Math.Max(1.0M, 1.00M)) |> equal "1.0" + string (Math.Min(1.0M, 1.00M)) |> equal "1.00" + testCase "Decimal rounding keeps the requested scale" <| fun () -> string (Math.Round(1.20001M, 2)) |> equal "1.20" string (Math.Round(1.2M, 3)) |> equal "1.2" From cd63e57e909393ab493a1d8bfedbea9a50a377c9 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 21 Oct 2026 18:52:51 +0200 Subject: [PATCH 204/322] feat(js/ts): map static Decimal conversion methods (#5129) --- src/Fable.Transforms/Replacements.fs | 7 +++++++ tests/Js/Main/ArithmeticTests.fs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index adcc960ad5..9449a9e1b5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2839,6 +2839,13 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Helper.LibCall(com, "Decimal", meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | ("ToByte" | "ToSByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToSingle" | "ToDouble"), + [ _ ] -> + match t with + | Number(BigIntegers _, _) -> toLong com ctx r t args |> Some + | Number(Integers _, _) -> toInt com ctx r t args |> Some + | Number(Floats _, _) -> toFloat com ctx r t args |> Some + | _ -> None | "ToString", [ ExprTypeAs(String, format) ] | "ToString", [ ExprTypeAs(String, format); _ (* Culture info *) ] -> numericStringFormat com ctx r t i thisArg format diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 43833cf96d..240e9f6115 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -583,6 +583,23 @@ let tests = string (Math.Max(1.0M, 1.00M)) |> equal "1.0" string (Math.Min(1.0M, 1.00M)) |> equal "1.00" + testCase "Decimal static conversions truncate and check the range" <| fun () -> + Decimal.ToInt32 2.9M |> equal 2 + Decimal.ToInt32 -2.9M |> equal -2 + Decimal.ToInt64 -2.9M |> equal -2L + Decimal.ToInt64 9223372036854775807.5M |> equal 9223372036854775807L + Decimal.ToUInt64 18446744073709551615M |> equal 18446744073709551615UL + Decimal.ToInt16 32767.5M |> equal 32767s + Decimal.ToUInt16 65535.9M |> equal 65535us + Decimal.ToByte 255.9M |> equal 255uy + Decimal.ToSByte -128.9M |> equal -128y + Decimal.ToUInt32 4294967295.9M |> equal 4294967295u + Decimal.ToDouble 0.1M |> equal 0.1 + Decimal.ToSingle 1.5M |> equal 1.5f + (fun () -> Decimal.ToInt32 2147483648M) |> throwsError "" + (fun () -> Decimal.ToByte -1M) |> throwsError "" + Decimal.ToUInt64 -0.5M |> equal 0UL + testCase "Decimal rounding keeps the requested scale" <| fun () -> string (Math.Round(1.20001M, 2)) |> equal "1.20" string (Math.Round(1.2M, 3)) |> equal "1.2" From eb68225ff74d1b455765d99998faa6fd3be32383 Mon Sep 17 00:00:00 2001 From: Simon Treanor <4550815+simontreanor@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:08:35 +0100 Subject: [PATCH 205/322] feat(js/ts): support System.Diagnostics.Stopwatch (#5100) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Replacements.fs | 32 +++++++++++ src/fable-library-ts/Stopwatch.ts | 81 ++++++++++++++++++++++++++++ tests/Js/Main/MiscTests.fs | 56 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/fable-library-ts/Stopwatch.ts diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 9449a9e1b5..2352579007 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -853,6 +853,7 @@ let tryEntityIdent (com: Compiler) entFullName = // | Naming.EndsWith "Exception" _ -> makeImportLib com Any "Exception" "Util" |> Some | Types.attribute -> makeImportLib com Any "Attribute" "Types" |> Some | "System.Uri" -> makeImportLib com Any "Uri" "Uri" |> Some + | "System.Diagnostics.Stopwatch" -> makeImportLib com Any "Stopwatch" "Stopwatch" |> Some | "Microsoft.FSharp.Control.FSharpAsyncReplyChannel`1" -> makeImportLib com Any "AsyncReplyChannel" "AsyncBuilder" |> Some | "Microsoft.FSharp.Control.FSharpEvent`1" -> makeImportLib com Any "Event" "Event" |> Some @@ -3804,6 +3805,33 @@ let timers (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | meth, Some x, args -> Helper.InstanceCall(x, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some | _ -> None +let stopwatch (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let toTimeSpan (ms: Expr) = + Helper.LibCall(com, timeSpanModule com, "fromMilliseconds", t, [ ms ], ?loc = r) + + let float64 = Number(Float64, NumberInfo.Empty) + + match i.CompiledName, thisArg with + | ".ctor", _ -> Helper.LibCall(com, "Stopwatch", "create", t, [], ?loc = r) |> Some + | ("StartNew" | "GetTimestamp" as meth), None -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst meth, t, [], ?loc = r) + |> Some + | ("Start" | "Stop" | "Reset" | "Restart" as meth), Some x -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst meth, t, [ x ], ?loc = r) + |> Some + | ("get_IsRunning" | "get_ElapsedMilliseconds" | "get_ElapsedTicks" as meth), Some x -> + Helper.LibCall(com, "Stopwatch", Naming.removeGetSetPrefix meth |> Naming.lowerFirst, t, [ x ], ?loc = r) + |> Some + | "get_Elapsed", Some x -> + Helper.LibCall(com, "Stopwatch", "elapsedTotalMilliseconds", float64, [ x ], ?loc = r) + |> toTimeSpan + |> Some + | "GetElapsedTime", None -> + Helper.LibCall(com, "Stopwatch", "getElapsedTotalMilliseconds", float64, args, ?loc = r) + |> toTimeSpan + |> Some + | _ -> None + let systemEnv (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "get_NewLine" -> Some(makeStrConst "\n") @@ -4700,6 +4728,9 @@ let tryField com returnTyp ownerTyp fieldName = | "System.BitConverter" -> Helper.LibCall(com, "BitConverter", Naming.lowerFirst fieldName, returnTyp, []) |> Some + | "System.Diagnostics.Stopwatch" when fieldName = "Frequency" || fieldName = "IsHighResolution" -> + Helper.LibCall(com, "Stopwatch", Naming.lowerFirst fieldName, returnTyp, []) + |> Some | _ -> None | _ -> None @@ -4798,6 +4829,7 @@ let private replacedModules = "System.Convert", convert "System.Console", console "System.Diagnostics.Debug", debug + "System.Diagnostics.Stopwatch", stopwatch "System.Diagnostics.Debugger", debug Types.datetime, dateTime Types.datetimeOffset, dateTimeOffset diff --git a/src/fable-library-ts/Stopwatch.ts b/src/fable-library-ts/Stopwatch.ts new file mode 100644 index 0000000000..b48178f11e --- /dev/null +++ b/src/fable-library-ts/Stopwatch.ts @@ -0,0 +1,81 @@ +import { int64 } from "./BigInt.ts"; + +// Timestamps are in microseconds, the resolution performance.now() can offer +const ticksPerMillisecond = 1000; + +export class Stopwatch { + // Milliseconds measured before the last Start + public elapsed = 0; + // performance.now() at the last Start, while running + public startedAt: number | undefined = undefined; +} + +export function create(): Stopwatch { + return new Stopwatch(); +} + +export function startNew(): Stopwatch { + const sw = new Stopwatch(); + start(sw); + return sw; +} + +export function start(sw: Stopwatch): void { + if (sw.startedAt === undefined) { + sw.startedAt = performance.now(); + } +} + +export function stop(sw: Stopwatch): void { + if (sw.startedAt !== undefined) { + sw.elapsed += performance.now() - sw.startedAt; + sw.startedAt = undefined; + } +} + +export function reset(sw: Stopwatch): void { + sw.elapsed = 0; + sw.startedAt = undefined; +} + +export function restart(sw: Stopwatch): void { + sw.elapsed = 0; + sw.startedAt = performance.now(); +} + +export function isRunning(sw: Stopwatch): boolean { + return sw.startedAt !== undefined; +} + +function rawElapsedMilliseconds(sw: Stopwatch): number { + return sw.elapsed + (sw.startedAt === undefined ? 0 : performance.now() - sw.startedAt); +} + +// In units of Frequency, like .NET (not TimeSpan ticks) +export function elapsedTicks(sw: Stopwatch): int64 { + return BigInt(Math.trunc(rawElapsedMilliseconds(sw) * ticksPerMillisecond)); +} + +export function elapsedMilliseconds(sw: Stopwatch): int64 { + return elapsedTicks(sw) / BigInt(ticksPerMillisecond); +} + +export function elapsedTotalMilliseconds(sw: Stopwatch): number { + return Number(elapsedTicks(sw)) / ticksPerMillisecond; +} + +export function frequency(): int64 { + return BigInt(ticksPerMillisecond * 1000); +} + +export function isHighResolution(): boolean { + return true; +} + +export function getTimestamp(): int64 { + return BigInt(Math.trunc(performance.now() * ticksPerMillisecond)); +} + +export function getElapsedTotalMilliseconds(startingTimestamp: int64, endingTimestamp?: int64): number { + return Number((endingTimestamp ?? getTimestamp()) - startingTimestamp) / ticksPerMillisecond; +} diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 41a54fcd46..5b3e359150 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -1465,4 +1465,60 @@ let tests = let times2 x = x * 2 fn <- fn >> times2 fn 5 |> equal 10 + + testCase "Stopwatch.Frequency and IsHighResolution work" <| fun () -> + Diagnostics.Stopwatch.Frequency > 0L |> equal true + Diagnostics.Stopwatch.IsHighResolution |> equal true + + testCaseAsync "Stopwatch.GetTimestamp and GetElapsedTime work" <| fun () -> + async { + let start = Diagnostics.Stopwatch.GetTimestamp() + do! Async.Sleep 50 + let stop = Diagnostics.Stopwatch.GetTimestamp() + let elapsedMs = float (stop - start) * 1000. / float Diagnostics.Stopwatch.Frequency + elapsedMs >= 40. |> equal true + (Diagnostics.Stopwatch.GetElapsedTime(start, stop)).TotalMilliseconds >= 40. |> equal true + (Diagnostics.Stopwatch.GetElapsedTime start).TotalMilliseconds >= 40. |> equal true + } + + testCaseAsync "Stopwatch.StartNew and Elapsed work" <| fun () -> + async { + let sw = Diagnostics.Stopwatch.StartNew() + box sw :? Diagnostics.Stopwatch |> equal true + sw.IsRunning |> equal true + do! Async.Sleep 50 + sw.ElapsedMilliseconds >= 40L |> equal true + sw.Elapsed.TotalMilliseconds >= 40. |> equal true + sw.ElapsedTicks >= Diagnostics.Stopwatch.Frequency / 25L |> equal true + } + + testCaseAsync "Stopwatch Start, Stop and Reset work" <| fun () -> + async { + let sw = Diagnostics.Stopwatch() + sw.IsRunning |> equal false + sw.ElapsedMilliseconds |> equal 0L + sw.Start() + do! Async.Sleep 50 + sw.Stop() + sw.IsRunning |> equal false + let stopped = sw.ElapsedMilliseconds + stopped >= 40L |> equal true + do! Async.Sleep 50 + sw.ElapsedMilliseconds |> equal stopped + sw.Start() + do! Async.Sleep 50 + sw.ElapsedMilliseconds >= stopped + 40L |> equal true + sw.Reset() + sw.IsRunning |> equal false + sw.ElapsedMilliseconds |> equal 0L + } + + testCaseAsync "Stopwatch.Restart works" <| fun () -> + async { + let sw = Diagnostics.Stopwatch.StartNew() + do! Async.Sleep 100 + sw.Restart() + sw.IsRunning |> equal true + sw.ElapsedMilliseconds < 50L |> equal true + } ] From d063630d58d62db21603d45a6bf54b172378c66e Mon Sep 17 00:00:00 2001 From: Simon Treanor <4550815+simontreanor@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:12:18 +0100 Subject: [PATCH 206/322] fix(js/ts): add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Replacements.fs | 36 +++- src/fable-library-ts/Async.ts | 116 ++++++++++-- src/fable-library-ts/Task.ts | 23 ++- tests/Js/Main/AsyncTests.fs | 257 +++++++++++++++++++++++++++ tests/Js/Main/TaskTests.fs | 60 +++++++ 5 files changed, 470 insertions(+), 22 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 2352579007..b934e06f84 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -4302,11 +4302,40 @@ let taskBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr ) |> Some -let tasks (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = +// .NET implements these overloads with Async.StartImmediateAsTask +let taskBuilderAsync (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let startAsPromise (computation: Expr) = + Helper.LibCall(com, "Async", "startAsPromise", Any, [ computation ], ?loc = r) + + match i.CompiledName, args with + | "TaskBuilderBase.Bind", [ computation; f ] -> taskBuilder com ctx r t i thisArg [ startAsPromise computation; f ] + | "TaskBuilderBase.ReturnFrom", [ computation ] -> taskBuilder com ctx r t i thisArg [ startAsPromise computation ] + | _ -> None + +let tasks (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match thisArg, i.CompiledName with - | Some x, ("GetAwaiter" | "GetResult" | "get_Result" | "Result") -> - // Task = Promise; return the promise - callers use Async.AwaitTask to extract the value + | Some x, "GetAwaiter" -> Some x + | Some x, ("GetResult" | "get_Result" | "Result") -> + "Task.Result and GetAwaiter().GetResult() return the task itself in JS/TS, as there's no way to block until it completes. Use let! in task { } or Async.AwaitTask instead." + |> addWarning com ctx.InlinePath r + Some x + | None, "Delay" -> + // Without Temporal, a TimeSpan is already a number of milliseconds + let args = + match args with + | ExprType(Builtin BclTimeSpan) as delay :: rest when com.Options.JsTemporal -> + Helper.LibCall( + com, + timeSpanModule com, + "totalMilliseconds", + Number(Float64, NumberInfo.Empty), + [ delay ] + ) + :: rest + | args -> args + + Helper.LibCall(com, "Task", "delay", t, args, ?loc = r) |> Some | None, "FromResult" -> Helper.LibCall(com, "Task", "fromResult", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -4875,6 +4904,7 @@ let private replacedModules = "Microsoft.FSharp.Control.TaskBuilderBase", taskBuilder "Microsoft.FSharp.Control.TaskBuilderModule", taskBuilder "Microsoft.FSharp.Control.TaskBuilderExtensions.HighPriority", taskBuilder + "Microsoft.FSharp.Control.TaskBuilderExtensions.MediumPriority", taskBuilderAsync "Microsoft.FSharp.Control.TaskBuilderExtensions.LowPriority", taskBuilder Types.guid, guids "System.Uri", uris diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index 75ea833b19..efda31ec03 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -5,7 +5,8 @@ import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts import type { IEvent$2 } from "./Event.ts"; import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts"; import { InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; -import { Exception } from "./Util.ts"; +import { Exception, ensureErrorOrException, IDisposable } from "./Util.ts"; +import type { Option } from "./Option.ts"; function emptyContinuation(_x: T) { // NOP @@ -54,33 +55,45 @@ export function throwIfCancellationRequested(token: CancellationToken) { } } +// The parent keeps the listener until unlink is called +function createLinkedToken(parent: CancellationToken): [token: CancellationToken, unlink: () => void] { + const token = new CancellationToken(); + const id = parent.addListener(() => token.cancel()); + return [token, () => { parent.removeListener(id); }]; +} + export function startChild(computation: Async, ms?: number): Async> { return protectedCont((ctx) => { - // Share the parent's cancellation token so cancelling the parent - // cancels the child, like .NET Async.StartChild - const promise = startAsPromise(computation, ctx.cancelToken); - let promiseToRun = promise; - - if (ms) { - // Race the computation against a timeout: whichever settles first wins. - promiseToRun = new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => reject(TimeoutException_$ctor()), ms); + const timeout = ms == null || ms === -1 ? undefined : ms; + const [childToken, unlink] = timeout === undefined + ? [ctx.cancelToken, () => { /* NOP */ }] + : createLinkedToken(ctx.cancelToken); + const promise = startAsPromise(computation, childToken); + let settled = false; + const onSettled = () => { settled = true; unlink(); }; + + // Without a rejection handler an unawaited child's failure is an unhandled rejection + promise.then(onSettled, onSettled); + + // Like .NET, the timeout only runs while the result is awaited, not from the start of the child + const awaitResult = (): Promise => + timeout === undefined || settled ? promise : new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + // .NET doesn't cancel the child when the timeout is 0 + if (timeout !== 0) { + try { childToken.cancel(); } catch (error) { reject(error); return; } + } + reject(TimeoutException_$ctor()); + }, timeout); promise.then( value => { clearTimeout(timeoutId); resolve(value); }, error => { clearTimeout(timeoutId); reject(error); } ); }); - } - - // Prevent an unhandled rejection when the child is cancelled or fails - // while nobody awaits it (in F# an unobserved child's cancellation or - // failure is simply not propagated). awaitPromise attaches its own - // handlers to the same promise, so awaiting still observes the result. - promiseToRun.catch(emptyContinuation); // JS Promises are hot, computation has already started // but we delay returning the result - protectedReturn(awaitPromise(promiseToRun))(ctx); + protectedReturn(protectedCont((awaitCtx) => awaitPromise(awaitResult())(awaitCtx)))(ctx); }); } @@ -175,6 +188,65 @@ export function parallel(computations: Iterable>) { awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w, ctx.cancelToken))))(ctx)); } +export function choice(computations: Iterable>>): Async> { + return protectedCont((ctx: IAsyncContext>) => { + const works = Array.from(computations); + let remaining = works.length; + if (remaining === 0) { + ctx.onSuccess(undefined); + return; + } + const [token, unlink] = createLinkedToken(ctx.cancelToken); + let done = false; + const finish = (report: () => void) => { + if (done) { return; } + done = true; + unlink(); + try { token.cancel(); } catch (error) { ctx.onError(ensureErrorOrException(error)); return; } + report(); + }; + for (const work of works) { + startWithContinuations( + work, + (x) => { + if (x != null) { + finish(() => ctx.onSuccess(x)); + } else if (--remaining === 0) { + finish(() => ctx.onSuccess(undefined)); + } + }, + (err) => finish(() => ctx.onError(err)), + (err) => finish(() => ctx.onCancel(err)), + token); + if (done) { break; } + } + }); +} + +export function onCancel(interruption: () => void): Async { + return protectedCont((ctx: IAsyncContext) => { + const token = ctx.cancelToken; + let interrupted = false; + const interrupt = () => { + if (!interrupted) { + interrupted = true; + try { interruption(); } catch { /* Ignored, like .NET Async.OnCancel */ } + } + }; + const id = token.addListener(interrupt); + let disposed = false; + ctx.onSuccess({ + Dispose() { + if (disposed) { return; } + disposed = true; + token.removeListener(id); + // With use! Dispose runs inside cancel() before this listener gets its turn + if (token.isCancelled) { interrupt(); } + }, + }); + }); +} + export function sequential(computations: Iterable>) { function _sequential(computations: Iterable>, cancelToken: CancellationToken): Promise { @@ -248,3 +320,11 @@ export function startAsPromise(computation: Async, cancellationToken?: Can startWithContinuations(computation, resolve, reject, reject, cancellationToken ? cancellationToken : defaultCancellationToken)); } + +export function startAsTask(computation: Async, _taskCreationOptions?: number, cancellationToken?: CancellationToken) { + return startAsPromise(computation, cancellationToken); +} + +export function startImmediateAsTask(computation: Async, cancellationToken?: CancellationToken) { + return startAsPromise(computation, cancellationToken); +} diff --git a/src/fable-library-ts/Task.ts b/src/fable-library-ts/Task.ts index 0bceaabaee..6e1cb1c369 100644 --- a/src/fable-library-ts/Task.ts +++ b/src/fable-library-ts/Task.ts @@ -1,4 +1,4 @@ -import { OperationCanceledException } from "./AsyncBuilder.ts"; +import { CancellationToken, OperationCanceledException } from "./AsyncBuilder.ts"; export class TaskCompletionSource { private _resolve!: (value: T) => void; @@ -26,6 +26,27 @@ export function zero(): Promise { return Promise.resolve(); } +// Like .NET, -1 waits until the token is cancelled +export function delay(millisecondsDelay: number, cancellationToken?: CancellationToken): Promise { + return new Promise((resolve, reject) => { + if (cancellationToken?.isCancelled) { + reject(new OperationCanceledException()); + return; + } + let tokenId: number | undefined; + const timeoutId = millisecondsDelay === -1 ? undefined : setTimeout(() => { + if (tokenId !== undefined) { cancellationToken!.removeListener(tokenId); } + resolve(); + }, millisecondsDelay); + if (cancellationToken != null) { + tokenId = cancellationToken.addListener(() => { + clearTimeout(timeoutId); + reject(new OperationCanceledException()); + }); + } + }); +} + // Task = Promise in JS/TS. GetAwaiter/GetResult return the Promise itself; // callers should use Async.AwaitTask to extract the value in an async context. export function getAwaiter(t: Promise): Promise { diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index 77a9965250..e94deeede0 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1359,4 +1359,261 @@ let tests = do! Async.Sleep 600 equal 0 completed } + + testCaseAsync "Async.StartChild with timeout cancels the child when the timeout expires" <| fun () -> + async { + let mutable completed = false + let child = async { + do! Async.Sleep 300 + completed <- true + } + let mutable timedOut = false + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with + | :? TimeoutException -> timedOut <- true + do! Async.Sleep 400 + equal true timedOut + equal false completed + } + + testCaseAsync "Async.StartChild with timeout leaves the parent running when the timeout expires" <| fun () -> + async { + let child = async { do! Async.Sleep 300 } + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with + | :? TimeoutException -> () + let! token = Async.CancellationToken + equal false token.IsCancellationRequested + } + + testCaseAsync "Async.StartChild timeout only runs while the result is awaited" <| fun () -> + async { + let child = async { + do! Async.Sleep 150 + return 42 + } + let! childResult = Async.StartChild(child, 100) + do! Async.Sleep 250 + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of -1 never times out" <| fun () -> + async { + let child = async { + do! Async.Sleep 50 + return 42 + } + let! childResult = Async.StartChild(child, -1) + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of 0 returns an available result" <| fun () -> + async { + let child = async { + do! Async.Sleep 50 + return 42 + } + let! childResult = Async.StartChild(child, 0) + do! Async.Sleep 200 + let! result = childResult + equal 42 result + } + + testCaseAsync "Async.StartChild with a timeout of 0 raises when the result isn't available and leaves the child running" <| fun () -> + async { + let mutable completed = false + let child = async { + do! Async.Sleep 300 + completed <- true + } + let mutable timedOut = false + try + let! childResult = Async.StartChild(child, 0) + do! childResult + with + | :? TimeoutException -> timedOut <- true + do! Async.Sleep 400 + equal true timedOut + equal true completed + } + + testCaseAsync "Async.StartChild with timeout raises the child's cancellation callback failure" <| fun () -> + async { + let child = async { + let! token = Async.CancellationToken + token.Register(fun () -> failwith "cleanup") |> ignore + do! Async.Sleep 300 + } + let mutable errors = -1 + try + let! childResult = Async.StartChild(child, 100) + do! childResult + with error -> +#if FABLE_COMPILER + errors <- Runtime.cancellationErrorCount error +#else + errors <- (error :?> AggregateException).InnerExceptions.Count +#endif + equal 1 errors + } + + testCaseAsync "Async.OnCancel ignores a failing interruption" <| fun () -> + async { + let mutable interrupted = false + let mutable cancelled = false + let cts = new CancellationTokenSource() + Async.StartWithContinuations( + async { + use! _ = Async.OnCancel(fun () -> + interrupted <- true + failwith "interruption") + do! Async.Sleep 300 + }, + ignore, + ignore, + (fun _ -> cancelled <- true), + cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal true interrupted + equal true cancelled + } + + testCaseAsync "Async.Choice returns the first Some and cancels the others" <| fun () -> + async { + let mutable completed = false + let fast = async { + do! Async.Sleep 20 + return Some 1 + } + let slow = async { + do! Async.Sleep 300 + completed <- true + return Some 2 + } + let none = async { return None } + let! result = Async.Choice [ slow; none; fast ] + do! Async.Sleep 400 + equal (Some 1) result + equal false completed + } + + testCaseAsync "Async.Choice returns None when every computation does" <| fun () -> + async { + let none = async { + do! Async.Sleep 10 + return (None: int option) + } + let! result = Async.Choice [ none; none ] + equal None result + let! empty = Async.Choice ([]: Async list) + equal None empty + } + + testCaseAsync "Async.Choice raises when a computation fails" <| fun () -> + async { + let fails = async { + do! Async.Sleep 10 + return failwith "boom" + } + let slow = async { + do! Async.Sleep 300 + return Some 1 + } + let mutable error = "" + try + let! _ = Async.Choice [ slow; fails ] + () + with ex -> error <- ex.Message + equal "boom" error + } + + testCaseAsync "Async.OnCancel runs the interruption when cancelled" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + use! _ = Async.OnCancel(fun () -> interrupted <- true) + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal true interrupted + } + + testCaseAsync "Async.OnCancel doesn't run the interruption once disposed" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + let! registration = Async.OnCancel(fun () -> interrupted <- true) + registration.Dispose() + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal false interrupted + } + + testCaseAsync "Async.OnCancel doesn't run the interruption when disposed before the cancellation and again by use!" <| fun () -> + async { + let mutable interrupted = false + let cts = new CancellationTokenSource() + Async.StartImmediate(async { + use! registration = Async.OnCancel(fun () -> interrupted <- true) + registration.Dispose() + do! Async.Sleep 300 + }, cts.Token) + do! Async.Sleep 50 + cts.Cancel() + do! Async.Sleep 50 + equal false interrupted + } + + testCaseAsync "Async.StartAsTask works" <| fun () -> + async { + let t = Async.StartAsTask(async { + do! Async.Sleep 10 + return 42 + }) + let! result = Async.AwaitTask t + equal 42 result + } + + testCaseAsync "Async.StartAsTask passes the cancellation token" <| fun () -> + async { + let mutable completed = false + let cts = new CancellationTokenSource() + let t = Async.StartAsTask(async { + do! Async.Sleep 200 + completed <- true + }, cancellationToken = cts.Token) + cts.Cancel() + let mutable outcome = "pending" + // AwaitTask reports a cancelled task as an exception on .NET and as a cancellation in JS + Async.StartWithContinuations( + Async.AwaitTask t, + (fun () -> outcome <- "completed"), + (fun _ -> outcome <- "cancelled"), + (fun _ -> outcome <- "cancelled")) + do! Async.Sleep 300 + equal "cancelled" outcome + equal false completed + } + + testCaseAsync "Async.StartImmediateAsTask works" <| fun () -> + async { + let t = Async.StartImmediateAsTask(async { return 42 }) + let! result = Async.AwaitTask t + equal 42 result + } ] diff --git a/tests/Js/Main/TaskTests.fs b/tests/Js/Main/TaskTests.fs index 392614cb0a..63e60d4006 100644 --- a/tests/Js/Main/TaskTests.fs +++ b/tests/Js/Main/TaskTests.fs @@ -174,4 +174,64 @@ let tests = let! result = awaitTask tcs.Task equal 42 result } + + testCaseAsync "Task.Delay works" <| fun () -> + async { + let mutable steps = [] + let! _ = awaitTask (task { + steps <- "before" :: steps + do! Task.Delay 20 + do! Task.Delay(TimeSpan.FromMilliseconds 10.) + steps <- "after" :: steps + }) + equal [ "after"; "before" ] steps + } + + testCaseAsync "Task.Delay can be cancelled" <| fun () -> + async { + let cts = new System.Threading.CancellationTokenSource() + let delay = Task.Delay(1000, cts.Token) + cts.Cancel() + let! cancelled = awaitTask (task { + try + do! delay + return false + with _ -> return true + }) + equal true cancelled + } + + testCaseAsync "task can bind an Async" <| fun () -> + async { + let work = async { + do! Async.Sleep 10 + return 41 + } + let! result = awaitTask (task { + let! x = work + return x + 1 + }) + equal 42 result + } + + testCaseAsync "task can return! an Async" <| fun () -> + async { + let work = async { + do! Async.Sleep 10 + return 42 + } + let! result = awaitTask (task { return! work }) + equal 42 result + } + + testCaseAsync "task can do! an Async" <| fun () -> + async { + let mutable ran = false + let work = async { + do! Async.Sleep 10 + ran <- true + } + let! _ = awaitTask (task { do! work }) + equal true ran + } ] From 5de3271d8b47f1752c4e74c010e4314cf40bbcbe Mon Sep 17 00:00:00 2001 From: Simon Treanor <4550815+simontreanor@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:46:12 +0100 Subject: [PATCH 207/322] fix(js/ts): respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) --- src/fable-library-ts/Async.ts | 56 ++++++++++++++++++++++++++++++++--- tests/Js/Main/AsyncTests.fs | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/fable-library-ts/Async.ts b/src/fable-library-ts/Async.ts index efda31ec03..c668bd85c0 100644 --- a/src/fable-library-ts/Async.ts +++ b/src/fable-library-ts/Async.ts @@ -4,7 +4,7 @@ import { Async, IAsyncContext, CancellationToken } from "./AsyncBuilder.ts"; import { protectedCont, protectedBind, protectedReturn } from "./AsyncBuilder.ts"; import type { IEvent$2 } from "./Event.ts"; import { FSharpChoice$2_$union, Choice_makeChoice1Of2, Choice_makeChoice2Of2 } from "./Choice.ts"; -import { InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; +import { ArgumentException_$ctor_Z384F8060, InvalidOperationException_$ctor_Z721C83C5, TimeoutException_$ctor } from "./System.ts"; import { Exception, ensureErrorOrException, IDisposable } from "./Util.ts"; import type { Option } from "./Option.ts"; @@ -182,10 +182,58 @@ export function ignore(computation: Async) { return protectedBind(computation, (_x) => protectedReturn(void 0)); } -export function parallel(computations: Iterable>) { - // Children share the parent's cancellation token, like .NET Async.Parallel +export function parallel(computations: Iterable>, maxDegreeOfParallelism?: number) { + if (maxDegreeOfParallelism != null && maxDegreeOfParallelism < 1) { + throw ArgumentException_$ctor_Z384F8060( + `maxDegreeOfParallelism must be positive, was ${maxDegreeOfParallelism}`, "maxDegreeOfParallelism"); + } return protectedCont((ctx: IAsyncContext) => - awaitPromise(Promise.all(Array.from(computations, (w) => startAsPromise(w, ctx.cancelToken))))(ctx)); + awaitPromise(runParallel(Array.from(computations), maxDegreeOfParallelism, ctx.cancelToken))(ctx)); +} + +function runParallel(computations: Async[], limit: number | undefined, parentToken: CancellationToken) { + return new Promise((resolve, reject) => { + const count = computations.length; + const results = new Array(count); + const [token, unlink] = createLinkedToken(parentToken); + let next = 0; + let running = 0; + let failed = false; + let failure: unknown; + + const startNext = () => { + const i = next++; + running++; + startAsPromise(computations[i], token) + .then( + (x) => { results[i] = x; }, + (err) => { + if (!failed) { + failed = true; + failure = err; + try { token.cancel(); } catch (error) { failure = error; } + } + }) + .then(() => { + running--; + if (!failed && next < count) { + startNext(); + } else if (running === 0) { + unlink(); + if (failed) { reject(failure); } else { resolve(results); } + } + }); + }; + + if (count === 0) { + unlink(); + resolve(results); + } + const initial = Math.min(limit ?? count, count); + while (next < initial) { + startNext(); + } + }); } export function choice(computations: Iterable>>): Async> { diff --git a/tests/Js/Main/AsyncTests.fs b/tests/Js/Main/AsyncTests.fs index e94deeede0..bcf3274a14 100644 --- a/tests/Js/Main/AsyncTests.fs +++ b/tests/Js/Main/AsyncTests.fs @@ -1485,6 +1485,60 @@ let tests = equal true interrupted equal true cancelled } + testCaseAsync "Async.Parallel respects maxDegreeOfParallelism" <| fun () -> + async { + let lockObj = obj () + let mutable running = 0 + let mutable peak = 0 + let work i = async { + lock lockObj (fun () -> + running <- running + 1 + peak <- max peak running) + do! Async.Sleep 50 + lock lockObj (fun () -> running <- running - 1) + return i + } + let! results = Async.Parallel([ for i in 1..10 -> work i ], maxDegreeOfParallelism = 3) + equal [| 1..10 |] results + equal 3 peak + } + + testCaseAsync "Async.Parallel cancels the other computations when one fails" <| fun () -> + async { + let mutable completed = 0 + let work i = async { + if i = 2 then + do! Async.Sleep 20 + failwith "boom" + do! Async.Sleep 300 +#if FABLE_COMPILER + completed <- completed + 1 +#else + System.Threading.Interlocked.Increment(&completed) |> ignore +#endif + } + let mutable error = "" + try + let! _ = Async.Parallel [ for i in 0..4 -> work i ] + () + with ex -> error <- ex.Message + do! Async.Sleep 400 + equal "boom" error + equal 0 completed + } + + testCaseAsync "Async.Parallel with an empty sequence returns an empty array" <| fun () -> + async { + let! results = Async.Parallel(([]: Async list), maxDegreeOfParallelism = 2) + equal 0 results.Length + } + + testCase "Async.Parallel rejects a maxDegreeOfParallelism below 1" <| fun () -> + let mutable raised = false + try + Async.Parallel([ async { return 1 } ], maxDegreeOfParallelism = 0) |> ignore + with :? ArgumentException -> raised <- true + equal true raised testCaseAsync "Async.Choice returns the first Some and cancels the others" <| fun () -> async { From 122ed9c082b029b9767b964177715bfc8b6faeb1 Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:07:18 +0800 Subject: [PATCH 208/322] docs(js/ts): document ??= and the directive prologue helpers (#4166) Co-authored-by: Mangel Maxime --- src/Fable.Core/Fable.Core.JsInterop.fs | 54 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/Fable.Core/Fable.Core.JsInterop.fs b/src/Fable.Core/Fable.Core.JsInterop.fs index f9841984db..ded58d88e0 100644 --- a/src/Fable.Core/Fable.Core.JsInterop.fs +++ b/src/Fable.Core/Fable.Core.JsInterop.fs @@ -3,7 +3,24 @@ module Fable.Core.JsInterop open System open Fable.Core -/// Compiles to ?? operator in JavaScript +/// +/// Compiles to the JavaScript nullish coalescing operator ??. +/// Returns when it is neither null nor undefined at runtime, +/// otherwise . +/// +/// +/// let name = maybeName ??= "anonymous" +/// +/// Compiles to: +/// +/// const name = maybeName ?? "anonymous"; +/// +/// +/// Value tested for null or undefined. +/// Value returned when is null or undefined. +/// +/// MDN: Nullish coalescing operator +/// [] let (??=) (nullable: 'T) (defaultValue: 'T) : 'T = nativeOnly @@ -48,21 +65,36 @@ let emitJsExpr<'T> (args: obj) (jsCode: string) : 'T = nativeOnly let emitJsStatement<'T> (args: obj) (jsCode: string) : 'T = nativeOnly /// -/// Emit a directive prologue at the top of the file (before the imports) -/// -/// This is useful when working with Next.js or other frameworks that require -/// a specific directive at the top of the file, such as "use client" or "use server". +/// Emits a directive prologue at the top of the file, before the imports. +/// Frameworks such as Next.js require directives like "use client" or "use server" there. /// -/// Directive text +/// +/// let x = 5 +/// emitJsTopDirectivePrologue "use server" +/// +/// Compiles to: +/// +/// "use server"; +/// export const x = 5; +/// +/// +/// Directive text. The quotes are added. let emitJsTopDirectivePrologue (text: string) : unit = nativeOnly /// -/// Emit a directive prologue at the calling position. -/// -/// This is useful when you need to emit a directive prologue for a specific part of the code, -/// such as "use client" or "use server". +/// Emits a directive prologue at the calling position. /// -/// Directive text +/// +/// let x = 5 +/// emitJsDirectivePrologue "use server" +/// +/// Compiles to: +/// +/// export const x = 5; +/// "use server"; +/// +/// +/// Directive text. The quotes are added. let emitJsDirectivePrologue (text: string) : unit = nativeOnly /// Create a literal JS object from a collection of key-value tuples. From 5586819c1405d4d846b8d7cd8eb2d23aaa39724c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:35:56 +0200 Subject: [PATCH 209/322] chore: Bump the all-nuget group with 4 updates (#5123) Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 2 +- src/Directory.Build.props | 4 ++-- src/Fable.Analyzers/Fable.Analyzers.fsproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 15b2c543a5..43464f190f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -17,7 +17,7 @@ "rollForward": false }, "fsharp-analyzers": { - "version": "0.40.0", + "version": "0.41.0", "commands": [ "fsharp-analyzers" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 62019bf3f9..869b16c3f7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -28,11 +28,11 @@ all build - + all analyzers - + all analyzers diff --git a/src/Fable.Analyzers/Fable.Analyzers.fsproj b/src/Fable.Analyzers/Fable.Analyzers.fsproj index dec98bff99..1eb602f578 100644 --- a/src/Fable.Analyzers/Fable.Analyzers.fsproj +++ b/src/Fable.Analyzers/Fable.Analyzers.fsproj @@ -10,7 +10,7 @@ - + From 5c7561b84e8eb596db9ae45e927791b302c68313 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:36:13 +0200 Subject: [PATCH 210/322] chore: Bump @testing-library/user-event from 14.6.7 to 14.6.8 in /tests/React in the all-npm group (#5119) Signed-off-by: dependabot[bot] --- tests/React/package-lock.json | 8 ++++---- tests/React/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index f9d368cd7b..18b9651e25 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -10,7 +10,7 @@ "@fluentui/react": "^8.125.8", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", - "@testing-library/user-event": "^14.6.7", + "@testing-library/user-event": "^14.6.8", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", @@ -1932,9 +1932,9 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.6.7", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", - "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "version": "14.6.8", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.8.tgz", + "integrity": "sha512-vgSSqnANMnkcYR/vUNDbwS7aM3OErRLNUG8vCVVlj4oc6cKffUrccX7UAS5AmJQQNZTSibi9z5GvvRZ0aYgUCQ==", "license": "MIT", "engines": { "node": ">=12", diff --git a/tests/React/package.json b/tests/React/package.json index 57fe06b0dd..49c6047c0f 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -12,7 +12,7 @@ "@fluentui/react": "^8.125.8", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", - "@testing-library/user-event": "^14.6.7", + "@testing-library/user-event": "^14.6.8", "jest": "^30", "jest-environment-jsdom": "^30", "react": "^18", From ba0427839107c157436479958898688952df05c3 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 14:35:40 +0200 Subject: [PATCH 211/322] perf(all): faster project cracking (#5136) --- src/Fable.Compiler/MSBuildCrackerResolver.fs | 136 +++++++++++++++++-- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index a658d1f9d7..528676e673 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -30,6 +30,9 @@ module private MSBuildCrackerResolver = type FullPath = string + [] + let private TelemetryOptOutVariable = "DOTNET_CLI_TELEMETRY_OPTOUT" + let private run_dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) (defines: string list) = backgroundTask { let psi = ProcessStartInfo "dotnet" @@ -49,6 +52,10 @@ module private MSBuildCrackerResolver = // https://github.com/fable-compiler/Fable/issues/4014 psi.Environment.Add("DOTNET_NOLOGO", "1") + // The dotnet CLI blocks on sending telemetry at exit, which can take seconds on a restricted network + if not (psi.Environment.ContainsKey TelemetryOptOutVariable) then + psi.Environment.Add(TelemetryOptOutVariable, "1") + if not (List.isEmpty defines) then let definesValue = defines |> String.concat ";" psi.Environment.Add("DefineConstants", definesValue) @@ -134,6 +141,68 @@ Exception: return tfs.Split ';' |> Array.head } + let private restoreInputNames = + [ + "Directory.Build.props" + "Directory.Build.targets" + "Directory.Packages.props" + "nuget.config" + "NuGet.Config" + "NuGet.config" + "global.json" + "packages.lock.json" + "paket.dependencies" + "paket.lock" + "paket.references" + ] + + /// Files that can change the package graph of a project: the ones next to it and in every directory above it + let private restoreInputs (projectFile: string) = + let rec walk (dir: DirectoryInfo) acc = + let acc = + restoreInputNames + |> List.map (fun name -> System.IO.Path.Combine(dir.FullName, name)) + |> List.filter File.Exists + |> List.append acc + + match dir.Parent with + | null -> acc + | parent -> walk parent acc + + walk (FileInfo(projectFile).Directory) [ projectFile ] + + /// Restore is skipped only when the last crack of this project, with the same defines and configuration, + /// left project.assets.json newer than every file the package graph depends on, for it and its references. + let private canSkipRestore (fsproj: FileInfo) (options: CrackerOptions) = + match options.CacheInfo with + | None -> false + | Some cache -> + let sameProject = + cache.ProjectPath = fsproj.FullName + && cache.Configuration = options.Configuration + && set cache.FableOptions.Define = set options.FableOptions.Define + + let assetsOf (project: string) = + FileInfo(System.IO.Path.Combine(System.IO.Path.GetDirectoryName project, "obj", "project.assets.json")) + + let assets = assetsOf fsproj.FullName + + let restoredAfterInputs (project: string) = + File.Exists project + && (assetsOf project).Exists + && restoreInputs project + |> List.forall (fun input -> File.GetLastWriteTimeUtc input < assets.LastWriteTimeUtc) + + sameProject + && assets.Exists + && fsproj.FullName :: cache.References |> List.forall restoredAfterInputs + + let private declaresTargetFrameworks (fsproj: FileInfo) = + try + File.ReadAllText(fsproj.FullName).Contains(" + true + let mkOptionsFromDesignTimeBuildAux (referenceTargetFrameworks: ConcurrentDictionary) (isMain: bool) @@ -148,21 +217,29 @@ Exception: else $"/p:Configuration=%s{options.Configuration}" - let! targetFramework = + // A single-target project evaluates its own TargetFramework, so the design-time build can report it + // and the separate query is only needed when the project multi-targets. + let! knownTargetFramework = match referenceTargetFrameworks.TryGetValue fsproj.FullName with - | true, targetFramework -> async.Return targetFramework - | _ -> queryTargetFramework fsproj options configurationProperty - + | true, targetFramework -> async.Return(Some targetFramework) + | _ when declaresTargetFrameworks fsproj -> + async { + let! targetFramework = queryTargetFramework fsproj options configurationProperty + return Some targetFramework + } + | _ -> async.Return None // When CoreCompile does not need a rebuild, MSBuild will skip that target and thus will not populate the FscCommandLineArgs items. // To overcome this we want to force a design time build, using the NonExistentFile property helps prevent a cache hit. let nonExistentFile = Path.Combine("__NonExistentSubDir__", "__NonExistentFile__") - let properties = + let properties targetFramework = [ if not (String.IsNullOrWhiteSpace configurationProperty) then configurationProperty - $"/p:TargetFramework=%s{targetFramework}" + match targetFramework with + | Some targetFramework -> $"/p:TargetFramework=%s{targetFramework}" + | None -> () "/p:DesignTimeBuild=True" "/p:SkipCompilerExecution=True" "/p:ProvideCommandLineArgs=True" @@ -178,6 +255,9 @@ Exception: let restoreArg = if options.NoRestore || not isMain then "" + elif canSkipRestore fsproj options then + Log.verbose (lazy $"Skipping restore of %s{fsproj.Name}: project.assets.json is up to date") + "" else let restoreProperties = [ @@ -197,13 +277,10 @@ Exception: $"/restore -restoreProperty:%s{restoreProperties}" - let arguments = - $"%s{restoreArg} /t:%s{targets} %s{properties} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType -warnAsMessage:NU1608" + let arguments targetFramework = + $"%s{restoreArg} /t:%s{targets} %s{properties targetFramework} --getItem:FscCommandLineArgs --getItem:ProjectReference --getItem:_MSBuildProjectReferenceExistent --getProperty:OutputType --getProperty:TargetFramework --getProperty:TargetFrameworks -warnAsMessage:NU1608" - let! json = - dotnet_msbuild_with_defines options.RootDir fsproj.FullName arguments options.FableOptions.Define - - let jsonDocument = + let parse (json: string) = try JsonDocument.Parse json with ex -> @@ -216,6 +293,41 @@ Exception: %O{ex} """ + let crack targetFramework = + async { + let! json = + dotnet_msbuild_with_defines + options.RootDir + fsproj.FullName + (arguments targetFramework) + options.FableOptions.Define + + return parse json + } + + let evaluatedTargetFramework (doc: JsonDocument) = + doc.RootElement.GetProperty("Properties").GetProperty("TargetFramework").GetString() + + let! jsonDocument, targetFramework = + async { + match knownTargetFramework with + | Some targetFramework -> + let! doc = crack (Some targetFramework) + return doc, targetFramework + | None -> + // A multi-target project not caught by the fsproj text check fails here or reports no + // framework, and takes the two-call path below. + let! attempt = crack None |> Async.Catch + + match attempt with + | Choice1Of2 doc when not (String.IsNullOrWhiteSpace(evaluatedTargetFramework doc)) -> + return doc, evaluatedTargetFramework doc + | _ -> + let! targetFramework = queryTargetFramework fsproj options configurationProperty + let! doc = crack (Some targetFramework) + return doc, targetFramework + } + let items = jsonDocument.RootElement.GetProperty "Items" let properties = jsonDocument.RootElement.GetProperty "Properties" From e9ffc41e5f53df018d4c141b9bfaa51ca76e0be5 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 16:17:35 +0200 Subject: [PATCH 212/322] perf(all): disable .NET telemetry for dotnet build (#5137) --- src/Fable.Compiler/MSBuildCrackerResolver.fs | 8 ++------ src/Fable.Compiler/ProjectCracker.fs | 6 +++++- src/Fable.Compiler/Util.fs | 15 +++++++++++++-- src/Fable.Compiler/Util.fsi | 5 +++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Fable.Compiler/MSBuildCrackerResolver.fs b/src/Fable.Compiler/MSBuildCrackerResolver.fs index 528676e673..52d95b3d3d 100644 --- a/src/Fable.Compiler/MSBuildCrackerResolver.fs +++ b/src/Fable.Compiler/MSBuildCrackerResolver.fs @@ -30,9 +30,6 @@ module private MSBuildCrackerResolver = type FullPath = string - [] - let private TelemetryOptOutVariable = "DOTNET_CLI_TELEMETRY_OPTOUT" - let private run_dotnet_msbuild (cwd: string) (fsproj: FullPath) (args: string) (defines: string list) = backgroundTask { let psi = ProcessStartInfo "dotnet" @@ -52,9 +49,8 @@ module private MSBuildCrackerResolver = // https://github.com/fable-compiler/Fable/issues/4014 psi.Environment.Add("DOTNET_NOLOGO", "1") - // The dotnet CLI blocks on sending telemetry at exit, which can take seconds on a restricted network - if not (psi.Environment.ContainsKey TelemetryOptOutVariable) then - psi.Environment.Add(TelemetryOptOutVariable, "1") + for key, value in Fable.Compiler.Util.Process.dotnetEnvVars () do + psi.Environment[key] <- value if not (List.isEmpty defines) then let definesValue = defines |> String.concat ";" diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index 9cf5b403ec..f381563ab6 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -158,7 +158,11 @@ type CrackerOptions(cliArgs: CliArgs, evaluateOnly: bool) = pendingDllBuilds[normalizedDllPath] <- Task.Run(fun () -> - Process.runSyncWithOutput projDir "dotnet" [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] + Process.runSyncWithOutputEnv + (Process.dotnetEnvVars ()) + projDir + "dotnet" + [ "build"; "-c"; cliArgs.Configuration; "--no-restore" ] ) member _.WaitForBuiltDlls() = diff --git a/src/Fable.Compiler/Util.fs b/src/Fable.Compiler/Util.fs index 9b938f5cbb..27ced7521a 100644 --- a/src/Fable.Compiler/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -474,13 +474,24 @@ module Process = let runSync (workingDir: string) (exePath: string) (args: string list) = runSyncWithEnv [] workingDir exePath args - let runSyncWithOutput workingDir exePath args = - let p = startProcess true [] workingDir exePath args + /// Environment for the `dotnet` processes Fable starts for itself, not for the user's --run commands + let dotnetEnvVars () = + // The dotnet CLI blocks on sending telemetry at exit, which can take seconds on a restricted network + if isNull (Environment.GetEnvironmentVariable "DOTNET_CLI_TELEMETRY_OPTOUT") then + [ "DOTNET_CLI_TELEMETRY_OPTOUT", "1" ] + else + [] + + let runSyncWithOutputEnv envVars workingDir exePath args = + let p = startProcess true envVars workingDir exePath args // Don't wait indefinitely to run process // This call is used to build local plugins, if the binary is used by another process this process will never end. p.WaitForExit 7000 |> ignore p.StandardOutput.ReadToEnd() + let runSyncWithOutput workingDir exePath args = + runSyncWithOutputEnv [] workingDir exePath args + [] module Async = let fold f (state: 'State) (xs: 'T seq) = diff --git a/src/Fable.Compiler/Util.fsi b/src/Fable.Compiler/Util.fsi index f856957270..4cecd1a3a4 100644 --- a/src/Fable.Compiler/Util.fsi +++ b/src/Fable.Compiler/Util.fsi @@ -94,6 +94,11 @@ module Process = val runSyncWithOutput: string -> string -> string list -> string + val runSyncWithOutputEnv: envVars: (string * string) list -> string -> string -> string list -> string + + /// Environment for the `dotnet` processes Fable starts for itself, not for the user's --run commands + val dotnetEnvVars: unit -> (string * string) list + type PathResolver = abstract TryPrecompiledOutPath: sourceDir: string * relativePath: string -> string option From 7a4e5396b09b8a9ff4516d4a7efc2e8d0816f2bb Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 16:52:19 +0200 Subject: [PATCH 213/322] docs(all): mention DOTNET_TieredPGO in help text (#5139) --- src/Fable.Cli/Entry.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Fable.Cli/Entry.fs b/src/Fable.Cli/Entry.fs index a12b69be37..e10a8fa026 100644 --- a/src/Fable.Cli/Entry.fs +++ b/src/Fable.Cli/Entry.fs @@ -159,6 +159,10 @@ Arguments: When set to '1' or 'true', Fable watch will poll the file system for changes. This is required for some file systems, such as network shares, Docker mounted volumes, and other virtual file systems. + + DOTNET_TieredPGO + When set to '0', non-watch compilations can run 10 to 20%% faster + depending on the project, at the cost of slower rebuilds in watch mode. """ let generateHelpWithPrefix prefixText = From 7cd1fd83486266f338b364845a9211bf6e6b1884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:31:16 +0200 Subject: [PATCH 214/322] chore: Bump pyo3 from 0.29.3 to 0.30.1 in /src/fable-library-py in the all-cargo group (#5120) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/fable-library-py/Cargo.lock | 41 +++++++++++++++++++++------------ src/fable-library-py/Cargo.toml | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/fable-library-py/Cargo.lock b/src/fable-library-py/Cargo.lock index 6473d44bc9..df179cfd77 100644 --- a/src/fable-library-py/Cargo.lock +++ b/src/fable-library-py/Cargo.lock @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.29.3" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655" +checksum = "229ca13b8a15bb0d1487b81ab975bfcd97513416499a8312011b6036e42e0de4" dependencies = [ "chrono", "libc", @@ -194,18 +194,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.29.3" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb" +checksum = "70130e2b6dd1ee589a8788026a429561ca5c40fa7a2fa7914c2282eddae24700" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.29.3" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330" +checksum = "5e7ff73c555c286d212d4ce7e5fbda845a973c4f61bd5f160f5f2c3c2a04d1c4" dependencies = [ "libc", "pyo3-build-config", @@ -213,26 +213,26 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.29.3" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6" +checksum = "c46d2e5862c7bb8207e3b639f713dda3c499933444784154c11d013c5a691b60" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 3.0.9", ] [[package]] name = "pyo3-macros-backend" -version = "0.29.3" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3" +checksum = "38b89f066f2ae72a2aef52d1537ca2fcd104440ebfadee06e97aa90738a25b9b" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -296,6 +296,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -340,7 +351,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -374,7 +385,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -385,7 +396,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/src/fable-library-py/Cargo.toml b/src/fable-library-py/Cargo.toml index 2901613ece..96a40a0c29 100644 --- a/src/fable-library-py/Cargo.toml +++ b/src/fable-library-py/Cargo.toml @@ -12,7 +12,7 @@ name = "_core" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.29.0", features = ["extension-module", "chrono"] } +pyo3 = { version = "0.30.1", features = ["extension-module", "chrono"] } byteorder = "1.5.0" chrono = "0.4.45" regex = "1.12.4" From f4a1a8cdae2e8ac81edbdc6eba36cc140234170b Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 17:42:03 +0200 Subject: [PATCH 215/322] feat(all): support suppressing warnings via `// fable-disable` comments (#4808) --- src/Fable.Cli/Main.fs | 56 ++- src/Fable.Compiler/Library.fs | 19 +- src/Fable.Transforms/Babel/Fable2Babel.fs | 8 +- src/Fable.Transforms/Beam/Fable2Beam.fs | 4 +- src/Fable.Transforms/Dart/Fable2Dart.fs | 8 +- src/Fable.Transforms/Dart/Replacements.fs | 60 ++- src/Fable.Transforms/FSharp2Fable.fs | 15 +- src/Fable.Transforms/Fable.Transforms.fsproj | 2 + src/Fable.Transforms/Global/Compiler.fs | 3 +- src/Fable.Transforms/Global/WarningCodes.fs | 164 +++++++ .../Global/WarningSuppression.fs | 437 ++++++++++++++++++ src/Fable.Transforms/Php/Fable2Php.fs | 8 +- .../Python/Fable2Python.Reflection.fs | 2 +- .../Python/Fable2Python.Transforms.fs | 6 +- src/Fable.Transforms/Python/PythonCompiler.fs | 4 +- src/Fable.Transforms/Python/Replacements.fs | 84 ++-- src/Fable.Transforms/Replacements.Util.fs | 9 + src/Fable.Transforms/Replacements.fs | 99 ++-- src/Fable.Transforms/Rust/Fable2Rust.fs | 8 +- src/Fable.Transforms/Rust/Replacements.fs | 78 +++- src/Fable.Transforms/State.fs | 44 +- src/Fable.Transforms/Transforms.Util.fs | 28 +- .../src/Fable.Standalone.fsproj | 2 + src/fable-standalone/src/Main.fs | 1 + .../Compiler/BehaviorWarningTests.fs | 117 +++++ .../Compiler/CompilerHelpersTests.fs | 61 +++ .../Compiler/CompilerMessagesTests.fs | 19 + .../Compiler/Fable.Tests.Compiler.fsproj | 3 + .../Compiler/IgnoredArgumentTests.fs | 203 ++++++++ tests/Integration/Compiler/Main.fs | 3 + tests/Integration/Compiler/Util/Compiler.fs | 20 +- .../Compiler/WarningSuppressionTests.fs | 322 +++++++++++++ 32 files changed, 1735 insertions(+), 162 deletions(-) create mode 100644 src/Fable.Transforms/Global/WarningCodes.fs create mode 100644 src/Fable.Transforms/Global/WarningSuppression.fs create mode 100644 tests/Integration/Compiler/BehaviorWarningTests.fs create mode 100644 tests/Integration/Compiler/IgnoredArgumentTests.fs create mode 100644 tests/Integration/Compiler/WarningSuppressionTests.fs diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 351d9ec0a3..d66ba6116f 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -16,7 +16,7 @@ open Fable.Compiler open Fable.Compiler.ProjectCracker open Fable.Compiler.Util -module private Util = +module Util = type PathResolver with static member Dummy = @@ -79,10 +79,18 @@ module private Util = | Severity.Error -> "error" | Severity.Info -> "info" + // MSBuild-style ` `: without the code there is no way to + // discover what to put in a `// fable-disable-line` comment, and rendering it here + // also makes Fable warnings parseable by IDEs and MSBuild loggers. + let tag = + match log.Code with + | Some code -> $"%s{log.Tag} %s{code}" + | None -> log.Tag + match log.Range with | Some r -> - $"%s{file}(%i{r.start.line},%i{r.start.column}): (%i{r.``end``.line},%i{r.``end``.column}) %s{severity} %s{log.Tag}: %s{log.Message}" - | None -> $"%s{file}(1,1): %s{severity} %s{log.Tag}: %s{log.Message}" + $"%s{file}(%i{r.start.line},%i{r.start.column}): (%i{r.``end``.line},%i{r.``end``.column}) %s{severity} %s{tag}: %s{log.Message}" + | None -> $"%s{file}(1,1): %s{severity} %s{tag}: %s{log.Message}" let logErrors rootDir (logs: LogEntry seq) = logs @@ -353,6 +361,18 @@ type FsWatcher(delayMs: int) = type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFiles: Fable.Compiler.File array) = + let sourceReader = lazy (snd (Fable.Compiler.File.MakeSourceReader sourceFiles)) + + // Shared by every file of the project: a warning can point at any file (inlined calls) and + // files compile in parallel. A new ProjectCracked is built on every watch rebuild, so this + // never outlives the source it was computed from. + let warningSuppression = + lazy + (WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader.Value + )) + member _.CliArgs = cliArgs member _.ProjectFile = cliArgs.ProjectFile member _.FableOptions = cliArgs.CompilerOptions @@ -390,9 +410,32 @@ type ProjectCracked(cliArgs: CliArgs, crackerResponse: CrackerResponse, sourceFi crackerResponse.OutputType, ?outDir = cliArgs.OutDir, ?watchDependencies = watchDependencies, - ?signatureDependencies = signatureDependencies + ?signatureDependencies = signatureDependencies, + warningSuppression = warningSuppression.Value ) + /// Problems with the `fable-disable` directives themselves (typo'd codes, directives that + /// suppress nothing). Only valid once every file has been compiled, since a directive in one + /// file can be what suppresses a warning raised while compiling another. + member _.DirectiveDiagnostics(files: string seq) = + warningSuppression.Value.GetDiagnostics(files) + |> List.map (fun (file, d) -> + let pos: Position = + { + line = d.Line + column = 0 + } + + LogEntry.Make( + Severity.Warning, + d.Message, + fileName = file, + range = SourceLocation.Create(pos, pos, file), + code = d.Code + ) + ) + |> Array.ofList + member _.MapSourceFiles(f) = ProjectCracked(cliArgs, crackerResponse, Array.map f sourceFiles) @@ -1597,6 +1640,11 @@ let private compilationCycle (state: State) (changes: ISet) = Array.append logs [| log |], deps, sigDeps ) + // Every file is compiled by now, so a directive that still hasn't suppressed anything + // really is unused - checking earlier would flag directives that only ever fire for a + // warning raised while some later file was being compiled. + let logs = Array.append logs (projCracked.DirectiveDiagnostics filesToCompile) + let state = { state with PendingFiles = [||] diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs index 64d0f0ebda..7d850edfa0 100644 --- a/src/Fable.Compiler/Library.fs +++ b/src/Fable.Compiler/Library.fs @@ -221,7 +221,12 @@ module CodeServices = opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + warningSuppression = + WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader + ) ) // TODO: make it configurable if FableTransforms.transformFile is applied? @@ -265,6 +270,9 @@ module CodeServices = async { let fableLibDir = Path.getRelativePath currentFile crackerResponse.FableLibDir + // No `warningSuppression`: this overload is handed an already type-checked + // project rather than a `SourceReader`, so there is no source to scan for + // `// fable-disable` comments. Not a bug, just a limitation of the entry point. let compiler: CompilerImpl = CompilerImpl( currentFile, @@ -361,6 +369,12 @@ module CodeServices = let opts = cliArgs.CompilerOptions + let warningSuppression = + WarningSuppression.Resolver.FromCompilerOptions( + crackerResponse.ProjectOptions.OtherOptions, + sourceReader + ) + let! results = dependentFiles |> Array.filter (fun filePath -> not (filePath.EndsWith(".fsi", StringComparison.Ordinal))) @@ -375,7 +389,8 @@ module CodeServices = opts, fableLibDir, crackerResponse.OutputType, - ?outDir = cliArgs.OutDir + ?outDir = cliArgs.OutDir, + warningSuppression = warningSuppression ) let outputPath = Path.ChangeExtension(currentFile, ".js") diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 0fc37fa99b..45749eb362 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -476,7 +476,7 @@ module Reflection = let transformTypeTest (com: IBabelCompiler) ctx range expr typ : Expression = let warnAndEvalToFalse msg = - "Cannot type test (evals to false): " + msg |> addWarning com [] range + WarningCodes.typeTestEvalsToFalse msg |> addWarningWithCode com [] range Expression.booleanLiteral (false) @@ -1845,7 +1845,7 @@ module Util = // Some (baseExpr, []) // default base constructor let range = baseCall |> Option.bind (fun x -> x.Range) - $"Ignoring base call for %s{entRef.FullName}" |> addWarning com [] range + WarningCodes.baseCallIgnored entRef.FullName |> addWarningWithCode com [] range None | Some _, _ -> @@ -5065,8 +5065,8 @@ module Compiler = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = BabelCompiler(com) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index 73a6506d27..afa3b7c3ef 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -3844,8 +3844,8 @@ let transformFile (com: Fable.Compiler) (file: File) : Beam.ErlModule = member _.AddWatchDependency(file) = com.AddWatchDependency(file) member _.AddSignatureDependency(file) = com.AddSignatureDependency(file) - member _.AddLog(msg, severity, ?range, ?fileName, ?tag) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName, ?tag, ?code) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) } let rec collectMemberDeclarations declarations = diff --git a/src/Fable.Transforms/Dart/Fable2Dart.fs b/src/Fable.Transforms/Dart/Fable2Dart.fs index d590902545..4b9f9196c1 100644 --- a/src/Fable.Transforms/Dart/Fable2Dart.fs +++ b/src/Fable.Transforms/Dart/Fable2Dart.fs @@ -2038,8 +2038,8 @@ module Util = args | Some(Fable.Value _ as e) -> - $"Ignoring base call for %s{classDecl.Entity.FullName}" - |> addWarning com [] e.Range + WarningCodes.baseCallIgnored classDecl.Entity.FullName + |> addWarningWithCode com [] e.Range [] | Some e -> @@ -3210,8 +3210,8 @@ module Compiler = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = DartCompiler(com) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 4766a44f1a..86ea19665a 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1412,7 +1412,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetEnumerator", Some c, _ -> stringToCharSeq c |> getEnumerator com r t |> Some | ("Contains" | "StartsWith" | "EndsWith" as meth), Some c, arg :: _ -> if List.isMultiple args then - addWarning com ctx.InlinePath r $"String.%s{meth}: second argument is ignored" + WarningCodes.stringComparisonIgnored |> addWarningWithCode com ctx.InlinePath r Helper.InstanceCall(c, Naming.lowerFirst meth, t, [ arg ], ?loc = r) |> Some | ReplaceName [ "ToUpper", "toUpperCase" @@ -2113,8 +2113,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2122,10 +2122,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style | ("Parse" | "TryParse") as meth, str :: _ -> @@ -2135,10 +2138,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style @@ -2739,8 +2745,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r Helper.LibCall( com, @@ -2759,7 +2764,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt match i.CompiledName with | "get_Out" -> typedObjExpr t [] |> Some // empty object | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | _ -> None @@ -2767,7 +2772,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -2889,11 +2894,33 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.InstanceCall(thisArg.Value, meth, t, args, ?loc = r) |> Some | meth -> + // Drops the IFormatProvider (and DateTimeStyles, where present), warning once per + // discarded argument. `Parse arg` with no extra argument discards nothing. let args = + // Passing InvariantCulture asks for exactly what Fable does, so nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with - // Ignore IFormatProvider + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] | _ -> args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst @@ -3524,7 +3551,8 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis match i.CompiledName with // TODO: Throw error for RunSynchronously | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index f14860ef00..7307c72835 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -43,7 +43,7 @@ let private transformBaseConsCall makeCall r Fable.Unit callInfo baseRef | None -> if not baseCons.IsImplicitConstructor then - "Only inheriting from primary constructors is supported" |> addWarning com [] r + WarningCodes.nonPrimaryBaseConstructor |> addWarningWithCode com [] r match makeCallFrom com ctx r Fable.Unit genArgs None baseArgs baseCons with | Fable.Call(_baseExpr, info, t, r) when not (com.Options.Language = Rust) -> @@ -583,8 +583,8 @@ let private transformUnionCaseTest | TypeScriptTaggedUnion _, _ | _, None -> () | _, Some _ -> - $"CaseTest attribute on '%s{unionCase.Name}' is ignored: it is only supported on TypeScriptTaggedUnion cases" - |> addWarning com ctx.InlinePath r + WarningCodes.caseTestIgnored unionCase.Name + |> addWarningWithCode com ctx.InlinePath r match unionPattern with | ErasedUnionCase -> return "Cannot test erased union cases" |> addErrorAndReturnNull com ctx.InlinePath r @@ -2933,9 +2933,8 @@ type FableCompiler(com: Compiler) = |> List.filter (fun (i, v) -> if ctx.CapturedBindings.Contains(i.Name) && canHaveSideEffects com v then if isIdentUsed i.Name resolved then - $"Inlined argument %s{i.Name} is being captured but is also used somewhere else. " - + "There's a risk of double evaluation." - |> addWarning com [] i.Range + WarningCodes.inlinedArgumentDoubleEvaluation i.Name + |> addWarningWithCode com [] i.Range true else @@ -2981,8 +2980,8 @@ type FableCompiler(com: Compiler) = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let rec attachClassMembers (com: FableCompiler) = diff --git a/src/Fable.Transforms/Fable.Transforms.fsproj b/src/Fable.Transforms/Fable.Transforms.fsproj index 738cab2bb8..86cbef389d 100644 --- a/src/Fable.Transforms/Fable.Transforms.fsproj +++ b/src/Fable.Transforms/Fable.Transforms.fsproj @@ -5,6 +5,8 @@ + + diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index d1e384ddd6..0820c45006 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -90,7 +90,8 @@ type Compiler = abstract AddSignatureDependency: file: string -> unit abstract AddLog: - msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string -> unit + msg: string * severity: Severity * ?range: SourceLocation * ?fileName: string * ?tag: string * ?code: string -> + unit type InlineExprLazy(f: Compiler -> InlineExpr) = let mutable value: InlineExpr voption = ValueNone diff --git a/src/Fable.Transforms/Global/WarningCodes.fs b/src/Fable.Transforms/Global/WarningCodes.fs new file mode 100644 index 0000000000..73a25e0d86 --- /dev/null +++ b/src/Fable.Transforms/Global/WarningCodes.fs @@ -0,0 +1,164 @@ +(* + Central registry for Fable warnings codes + + - `FABLE0001`-`FABLE0099` describe Fable's own suppression directivers. They are never suppressible. + - `FABLE0100` and up describe code from compilation result. Those can be suppressed +*) +module Fable.Transforms.WarningCodes + +/// Can a `// fable-disable` comment silence a warning carrying this code? Codes below +/// `FABLE0100` describe the directives themselves and never can - see the band rules above. +/// This is what makes the banding an invariant rather than a naming convention: it holds +/// wherever a diagnostic is raised from, not just on the path that happens to raise them today. +/// +/// `None` is a warning with no code at all, which a bare `// fable-disable-line` still catches. +let isSuppressible (code: string option) = + match code with + | None -> true + // Every code is `FABLE` plus four digits, so ordinal comparison orders them numerically. + | Some code -> System.String.CompareOrdinal(code, "FABLE0100") >= 0 + +(* + FABLE0001-0099: about Fable's own directives. Never suppressible. +*) + +/// A `fable-disable*` directive named something that isn't in this registry - most likely a typo. +let unknownSuppressionCode (code: string) = + "FABLE0001", $"Unknown warning code '%s{code}' in a fable-disable directive" + +/// A `fable-disable*` directive that never suppressed anything, so it can be deleted. +let unusedSuppressionDirective = + "FABLE0002", "This fable-disable directive doesn't suppress anything" + +/// A bare `// fable-disable` block, which would silence every Fable warning up to end of file. +let suppressionBlockWithoutCode = + "FABLE0003", + "A 'fable-disable' block must list the warning codes it suppresses, otherwise it silences every Fable warning until the end of the file" + +(* + FABLE0100 and up: about the compiled code. Suppressible. +*) + +/// `String.StartsWith`/`EndsWith` with a `CultureInfo` argument: the comparison always runs +/// with the target's default culture rules, the argument is accepted but has no effect. +/// Used in both the JS/TS and Python replacements. +let cultureInfoIgnored = "FABLE0100", "CultureInfo argument is ignored" + +/// Dart's `contains`/`startsWith`/`endsWith` are ordinal and case-sensitive, so a +/// `StringComparison` or `CultureInfo` asking for anything else has no effect. +let stringComparisonIgnored = "FABLE0101", "String comparison argument is ignored" + +/// An `IFormatProvider`/`CultureInfo` passed to a `Parse`/`TryParse`, `String.Format` or +/// `StringBuilder.AppendFormat` overload. Fable is culture-independent here, so a culture that +/// changes the decimal separator or the day/month order silently changes the result. +let formatProviderIgnored = "FABLE0102", "Format provider argument is ignored" + +/// A `NumberStyles` value that isn't `Integer` or `HexNumber` passed to a numeric `Parse`. The +/// value is interpolated because the range can't tell you which style was discarded. +let numberStylesIgnored (style: int) = + "FABLE0103", $"NumberStyles argument %d{style} is ignored" + +/// A `DateTimeStyles` value passed to a date/time `Parse`. +let dateTimeStylesIgnored = "FABLE0104", "DateTimeStyles argument is ignored" + +/// A `TimeSpan` constructed with a microseconds argument. The runtime representation only carries +/// milliseconds, so the finer component is dropped rather than rounded. +let timeSpanPrecisionIgnored = + "FABLE0105", "TimeSpan precision is limited to milliseconds, microsecond arguments are ignored" + +/// `FSharpType.IsUnion(t, allowAccessToPrivateRepresentation)` and friends. Fable's reflection +/// has no notion of a private representation, so the flag never restricts anything. +let privateRepresentationFlagIgnored = + "FABLE0106", "Private representation flag is ignored" + +/// `Convert.ToBase64String(bytes, offset, length)` / `(bytes, options)`. Only the array is used, +/// so a slice is encoded whole and line-break options have no effect. +let base64ArgumentsIgnored = + "FABLE0107", "Base64 offset, length and formatting arguments are ignored" + +/// `Console.Write` and `Debug.Write`: the target cannot print without a line break. +let writeBehavesAsWriteLine = "FABLE0108", "Write will behave as WriteLine" + +/// `Async.Start`: there is no thread pool, so the computation starts on the current thread. +let asyncStartBehavesAsStartImmediate = + "FABLE0109", "Async.Start will behave as StartImmediate" + +/// `Task.Result` or `GetAwaiter().GetResult()` on JS/TS, where a task is a promise and nothing can block on it. +let taskResultReturnsTask = + "FABLE0110", + "Task.Result and GetAwaiter().GetResult() return the task itself in JS/TS, as there's no way to block until it completes. Use let! in task { } or Async.AwaitTask instead." + +/// `GetType()` on a value typed `obj`: Fable resolves types at compile time only. +let getTypeOnObj = + "FABLE0111", "Types can only be resolved at compile time. At runtime this will be same as `typeof`" + +/// A `:?` test against a type with no runtime representation (interface, erased type, Pojo class). +let typeTestEvalsToFalse (typeName: string) = + "FABLE0112", $"Cannot type test (evals to false): %s{typeName}" + +/// An `inherit` call whose arguments the target cannot forward to the base constructor. +let baseCallIgnored (entityFullName: string) = + "FABLE0113", $"Ignoring base call for %s{entityFullName}" + +/// An `inherit` through a secondary constructor (`new(...) = ...`). +let nonPrimaryBaseConstructor = + "FABLE0114", "Only inheriting from primary constructors is supported" + +/// An argument of an inlined function that is captured by a closure and also used elsewhere in the body. +let inlinedArgumentDoubleEvaluation (argName: string) = + "FABLE0115", + $"Inlined argument %s{argName} is being captured but is also used somewhere else. There's a risk of double evaluation." + +/// A `CaseTest` attribute on a case of a union that is not a `TypeScriptTaggedUnion`. +let caseTestIgnored (caseName: string) = + "FABLE0116", $"CaseTest attribute on '%s{caseName}' is ignored: it is only supported on TypeScriptTaggedUnion cases" + +/// Rust: `String.Compare` and `String.CompareTo` are compiled as ordinal comparisons. +let stringCompareIsOrdinal (methodName: string) = + "FABLE0117", $"String.%s{methodName} will be compiled as String.CompareOrdinal" + +/// Rust: a `char` is a Unicode scalar value, so it can never be a surrogate. +let charSurrogateTestsAreFalse = + "FABLE0118", "Rust chars are Unicode scalar values, so surrogate tests will be false." + +/// Rust: static and module `do` bindings are skipped unless the `static_do_bindings` feature is enabled. +let staticDoBindingsDisabled = + "FABLE0119", + "For Rust, support for F# static and module do bindings is disabled by default. It can be enabled with the 'static_do_bindings' feature. Use at your own risk!" + +/// PHP: a regex flag with no PCRE modifier, dropped from the pattern. +let regexFlagUnsupported (flag: string) = + "FABLE0120", $"Regex %s{flag} flag is not supported in Php" + +/// Every code the compiler can emit, both bands. A directive naming anything else is reported as +/// a typo, so a new warning MUST be added to this list as well as defined above. +let knownCodes = + [ + // The two parameterised warnings get a throwaway argument; only the code is read. + unknownSuppressionCode "" + unusedSuppressionDirective + suppressionBlockWithoutCode + cultureInfoIgnored + stringComparisonIgnored + formatProviderIgnored + numberStylesIgnored 0 + dateTimeStylesIgnored + timeSpanPrecisionIgnored + privateRepresentationFlagIgnored + base64ArgumentsIgnored + writeBehavesAsWriteLine + asyncStartBehavesAsStartImmediate + taskResultReturnsTask + getTypeOnObj + typeTestEvalsToFalse "" + baseCallIgnored "" + nonPrimaryBaseConstructor + inlinedArgumentDoubleEvaluation "" + caseTestIgnored "" + stringCompareIsOrdinal "" + charSurrogateTestsAreFalse + staticDoBindingsDisabled + regexFlagUnsupported "" + ] + |> List.map fst + |> Set.ofList diff --git a/src/Fable.Transforms/Global/WarningSuppression.fs b/src/Fable.Transforms/Global/WarningSuppression.fs new file mode 100644 index 0000000000..fa3c32182e --- /dev/null +++ b/src/Fable.Transforms/Global/WarningSuppression.fs @@ -0,0 +1,437 @@ +(* + Computes which diagnostics `// fable-disable/-enable...` comments suppress (ESLint's + disable-line/next-line/block model), via real comment tokens - not raw text matching. +*) +module Fable.Transforms.WarningSuppression + +open Fable +open System.Collections.Concurrent +open System.Text.RegularExpressions +open FSharp.Compiler.Tokenization + +[] +type private DirectiveKind = + | DisableLine + | DisableNextLine + | Disable + | Enable + +[] +type private Directive = + { + Kind: DirectiveKind + /// `None` means "every code" + Codes: Set option + Line: int + /// Set the first time this directive suppresses something. + mutable Used: bool + /// Set when the directive was already reported for something else (typo'd code, bare + /// block): no point telling the user it is unused on top of that. + mutable Reported: bool + } + +type private BlockState = + | NoneDisabled + /// Blanket disable opened by this directive, minus the codes since re-enabled. + | AllDisabledExcept of opener: Directive * enabled: Set + /// code -> the directive that disabled it + | SpecificDisabled of Map + +/// A problem with a directive itself (typo'd code, directive that suppresses nothing, ...), +/// reported once the file it belongs to has finished compiling. +type DirectiveDiagnostic = + { + Code: string + Message: string + Line: int + } + +// The name must be followed by end-of-comment, whitespace or `:` so that `fable-disabled` or +// `fable-disable-lines` aren't mistaken for a bare directive. `:` is then optionally consumed +let private directiveRegex = + Regex(@"^fable-(disable-next-line|disable-line|disable|enable)(?=$|\s|:)\s*:?\s*(.*)$", RegexOptions.Compiled) + +let private codeSeparators = [| ' '; ','; '\t' |] + +/// Cheap pre-filter: tokenizing a source file to look for directives that aren't there is pure +/// waste, and the vast majority of files contain none. +let private mayContainDirectives (source: string) = + source.IndexOf("fable-disable", System.StringComparison.Ordinal) >= 0 + || source.IndexOf("fable-enable", System.StringComparison.Ordinal) >= 0 + +/// Splits a directive's argument text into codes, dropping the ESLint-style ` -- justification` +/// tail. Codes are upper-cased so `fable0001` matches, and anything not in the registry is +/// returned separately so the caller can report the typo instead of silently ignoring it. +let private parseCodes (raw: string) : Set option * string list = + let raw = + match raw.IndexOf("--", System.StringComparison.Ordinal) with + | -1 -> raw + | i -> raw.Substring(0, i) + + let tokens = + raw.Split(codeSeparators, System.StringSplitOptions.RemoveEmptyEntries) + |> Array.map (fun t -> t.ToUpperInvariant()) + + let known, unknown = tokens |> Array.partition WarningCodes.knownCodes.Contains + + let codes = + if Array.isEmpty tokens then + None + else + Some(Set.ofArray known) + + codes, List.ofArray unknown + +let private stripCommentMarkers (raw: string) = + let raw = + if raw.StartsWith("//", System.StringComparison.Ordinal) then + raw.Substring(2) + elif raw.StartsWith("(*", System.StringComparison.Ordinal) then + raw.Substring(2) + else + raw + + let raw = + if raw.EndsWith("*)", System.StringComparison.Ordinal) then + raw.Substring(0, raw.Length - 2) + else + raw + + raw.Trim() + +let private tryParseDirective (line: int) (commentText: string) : (Directive * string list) option = + let text = stripCommentMarkers commentText + let m = directiveRegex.Match(text) + + if not m.Success then + None + else + let codes, unknown = parseCodes m.Groups[2].Value + + let kind = + match m.Groups[1].Value with + | "disable-line" -> Some DisableLine + | "disable-next-line" -> Some DisableNextLine + | "disable" -> Some Disable + | "enable" -> Some Enable + | _ -> None + + kind + |> Option.map (fun kind -> + { + Kind = kind + Codes = codes + Line = line + Used = false + Reported = false + }, + unknown + ) + +/// Gathers each line's comment token runs as plain text (a line can have more than one, e.g. a +/// block comment then a trailing line comment); also returns the lexer state to carry into the +/// next line, needed to resume correctly inside multi-line block comments/strings. +let private scanLineComments + (tokenizer: FSharpLineTokenizer) + (initialState: FSharpTokenizerLexState) + (line: string) + : string list * FSharpTokenizerLexState + = + // Each finished run is one comment on the line; `current` is the run being built. + let runs = ResizeArray() + let mutable current: System.Text.StringBuilder option = None + + // Pull tokens one at a time, threading the lexer state (needed across lines too). + let rec loop state = + match tokenizer.ScanToken(state) with + | Some(tok: FSharpTokenInfo), state2 -> + if tok.ColorClass = FSharpTokenColorKind.Comment then + let text = line.Substring(tok.LeftColumn, tok.RightColumn - tok.LeftColumn + 1) + + match current with + // Still inside the same comment: glue this token onto the current run. + | Some sb -> sb.Append(text) |> ignore + // First comment token after non-comment text: start a new run. + | None -> + let sb = System.Text.StringBuilder(text: string) + current <- Some sb + runs.Add(sb) + else + // Non-comment token: close the current run, if any (e.g. code between two comments). + current <- None + + loop state2 + // No more tokens on this line: return the final state for the next line. + | None, state2 -> state2 + + let endState = loop initialState + // Materialize each run's text; endState lets the caller resume correctly on the next line. + (runs |> Seq.map _.ToString() |> List.ofSeq), endState + +/// Computed, queryable suppression info for a single source file. +type FileSuppressions = + private + { + /// Every parsed directive, in source order. + Directives: Directive array + /// 1-based line -> the line-scoped directives landing on it + LineOnly: Map + /// index (line - 1) -> block-disable state as of (and including) that line + BlockAtLine: BlockState array + /// Problems found while parsing (typo'd codes, bare block disables) + ParseDiagnostics: DirectiveDiagnostic list + } + + /// Is a diagnostic with the given code (None = no code assigned to it) suppressed anywhere in + /// `[startLine, endLine]`? Line-scoped directives match on any line of the range, so a + /// trailing `// fable-disable-line` still works on a multi-line expression. The block state is + /// read at `startLine` only: a `fable-disable` buried inside a large expression shouldn't + /// retroactively silence a warning anchored above it. + member this.IsSuppressed(startLine: int, endLine: int, code: string option) = + let matches (d: Directive) = + match d.Codes with + | None -> true + | Some codes -> code |> Option.map codes.Contains |> Option.defaultValue false + + let mutable suppressed = false + + for line in startLine .. max startLine endLine do + match Map.tryFind line this.LineOnly with + | Some directives -> + for d in directives do + if matches d then + d.Used <- true + suppressed <- true + | None -> () + + if not suppressed && startLine >= 1 && startLine <= this.BlockAtLine.Length then + match this.BlockAtLine[startLine - 1] with + | NoneDisabled -> () + | AllDisabledExcept(opener, enabled) -> + let hit = + match code with + | None -> true + | Some c -> not (Set.contains c enabled) + + if hit then + opener.Used <- true + suppressed <- true + | SpecificDisabled disabled -> + match code |> Option.bind (fun c -> Map.tryFind c disabled) with + | Some opener -> + opener.Used <- true + suppressed <- true + | None -> () + + suppressed + + member this.IsSuppressed(line: int, code: string option) = this.IsSuppressed(line, line, code) + + /// Parse problems plus every directive that never suppressed anything. Only meaningful once + /// the whole compilation is over: an inlined call can suppress through a directive in a file + /// other than the one currently being compiled. + member this.GetDiagnostics() = + let unused = + this.Directives + |> Array.choose (fun d -> + // `fable-enable` re-opens warnings rather than suppressing them, so "unused" has + // no meaning for it - ESLint doesn't report those either. A directive whose codes + // are already reported as typos is obviously unused too; one message is enough. + if d.Kind = Enable || d.Used || d.Reported then + None + else + let code, message = WarningCodes.unusedSuppressionDirective + + Some + { + Code = code + Message = message + Line = d.Line + } + ) + |> List.ofArray + + this.ParseDiagnostics @ unused + + static member Empty = + { + Directives = [||] + LineOnly = Map.empty + BlockAtLine = [||] + ParseDiagnostics = [] + } + +/// Folds one `fable-disable`/`fable-enable` into the running block state. The directive itself is +/// carried through so that whatever it ends up suppressing can mark it used. +let private transition (state: BlockState) (d: Directive) = + match d.Kind = Disable, d.Codes, state with + | true, None, _ -> AllDisabledExcept(d, Set.empty) + | true, Some codes, NoneDisabled -> SpecificDisabled(codes |> Seq.map (fun c -> c, d) |> Map.ofSeq) + | true, Some codes, SpecificDisabled m -> SpecificDisabled((m, codes) ||> Set.fold (fun m c -> Map.add c d m)) + | true, Some codes, AllDisabledExcept(opener, ex) -> AllDisabledExcept(opener, Set.difference ex codes) + | false, None, _ -> NoneDisabled + | false, Some _, NoneDisabled -> NoneDisabled + | false, Some codes, SpecificDisabled m -> SpecificDisabled((m, codes) ||> Set.fold (fun m c -> Map.remove c m)) + | false, Some codes, AllDisabledExcept(opener, ex) -> AllDisabledExcept(opener, Set.union ex codes) + +let private computeDirectives (defines: string list) (source: string) = + let lines = source.Replace("\r\n", "\n").Split('\n') + let sourceTok = FSharpSourceTokenizer(defines, None, None, None) + let directives = ResizeArray() + let parseDiagnostics = ResizeArray() + let mutable state = FSharpTokenizerLexState.Initial + + for i in 0 .. lines.Length - 1 do + let lineNo = i + 1 + let tokenizer = sourceTok.CreateLineTokenizer(lines[i]) + let runs, newState = scanLineComments tokenizer state lines[i] + state <- newState + + for run in runs do + match tryParseDirective lineNo run with + | None -> () + | Some(directive, unknownCodes) -> + let flag (code, message) = + directive.Reported <- true + + parseDiagnostics.Add + { + Code = code + Message = message + Line = lineNo + } + + for unknown in unknownCodes do + flag (WarningCodes.unknownSuppressionCode unknown) + + // A bare `fable-disable` block silences every Fable warning to end of file, which + // is almost never what people mean - flag it, but honour it. + if directive.Kind = Disable && Option.isNone directive.Codes then + flag WarningCodes.suppressionBlockWithoutCode + + directives.Add directive + + let lineOnly = + (Map.empty, directives) + ||> Seq.fold (fun acc d -> + let add line = + let existing = Map.tryFind line acc |> Option.defaultValue [] + Map.add line (d :: existing) acc + + match d.Kind with + | DisableLine -> add d.Line + | DisableNextLine -> add (d.Line + 1) + | Disable + | Enable -> acc + ) + + let blockDirectivesByLine = + directives + |> Seq.filter (fun d -> + match d.Kind with + | Disable + | Enable -> true + | DisableLine + | DisableNextLine -> false + ) + |> Seq.groupBy (fun d -> d.Line) + |> Seq.map (fun (line, ds) -> line, List.ofSeq ds) + |> Map.ofSeq + + // Filling this with hundreds/thousands of NoneDisabled is nearly free: F# compiles a + // nullary DU case to a singleton, so every slot is a pointer to the same object. + let blockAtLine = Array.create lines.Length NoneDisabled + let mutable current = NoneDisabled + + for lineNo in 1 .. lines.Length do + match Map.tryFind lineNo blockDirectivesByLine with + | Some ds -> + for d in ds do + current <- transition current d + | None -> () + + blockAtLine[lineNo - 1] <- current + + { + Directives = Seq.toArray directives + LineOnly = lineOnly + BlockAtLine = blockAtLine + ParseDiagnostics = List.ofSeq parseDiagnostics + } + +/// Scans the given source text for `fable-disable*`/`fable-enable*` comments and builds a +/// queryable `FileSuppressions` snapshot. Meant to be computed once per file and cached. +/// +/// `defines` must be the same conditional-compilation symbols FCS was given (`FABLE_COMPILER`, +/// the project's `DefineConstants`, ...). Without them the tokenizer reports everything inside +/// an `#if` block as inactive code rather than comments, so directives sitting next to the very +/// warnings they're meant to suppress would be invisible. +let compute (defines: string list) (source: string) : FileSuppressions = + if mayContainDirectives source then + computeDirectives defines source + else + FileSuppressions.Empty + +/// Suppression snapshots shared by every file of a project: computing them is per-file work, but +/// warnings can point at any file (inlined calls) and files compile in parallel. Keyed by the +/// content hash `SourceReader` already returns, so a file edited under watch mode recomputes +/// instead of being scanned against stale text. +type private Cache() = + let entries = ConcurrentDictionary() + + /// May throw `KeyNotFoundException` for a file outside the project (e.g. the source of a + /// precompiled dll) - that's the caller's cue that there's nothing to suppress with. + member _.GetOrCompute(fileName: string, defines: string list, read: SourceReader) = + let hash, source = read fileName + + // GetOrAdd rather than a TryGetValue/assign pair: two threads racing on the same file + // would otherwise end up with separate snapshots, splitting the "directive was used" + // flags between them and producing bogus unused-directive reports. + let cachedHash, suppressions = + entries.GetOrAdd(fileName, fun _ -> hash, compute defines source.Value) + + if cachedHash = hash then + suppressions + else + // The file changed under us, so the snapshot is against the wrong text. + let fresh = hash, compute defines source.Value + entries[fileName] <- fresh + snd fresh + +/// Extracts the conditional-compilation symbols out of the option list FCS is given +/// (`FSharpProjectOptions.OtherOptions`), which is the only place the project's `DefineConstants` +/// and Fable's own `FABLE_COMPILER*` symbols are both present. +let definesFromCompilerOptions (otherOptions: string seq) = + otherOptions + |> Seq.choose (fun opt -> + if opt.StartsWith("--define:", System.StringComparison.Ordinal) then + Some(opt.Substring("--define:".Length)) + else + None + ) + |> List.ofSeq + +/// Project-wide entry point: resolves a file name to its suppressions, computing and caching on +/// first use. One instance per compiled project - it is shared across the per-file compilers. +type Resolver(defines: string list, read: SourceReader) = + let cache = Cache() + + static member FromCompilerOptions(otherOptions: string seq, read: SourceReader) = + Resolver(definesFromCompilerOptions otherOptions, read) + + member _.For(fileName: string) = + try + cache.GetOrCompute(fileName, defines, read) + with :? System.Collections.Generic.KeyNotFoundException -> + // Not a file of this project (e.g. the source of a precompiled dll): nothing to read, + // so nothing can be suppressed. Any other failure is a real problem and must surface. + FileSuppressions.Empty + + /// The directive problems of the given files. These are `FABLE0001`-band codes and so are + /// never themselves suppressible - they flag comment text that is wrong to keep, so the only + /// correct answer is to edit it. Call once the whole compilation is over: a warning raised + /// while compiling one file can be suppressed by a directive living in another. + member this.GetDiagnostics(fileNames: string seq) = + fileNames + |> Seq.collect (fun fileName -> this.For(fileName).GetDiagnostics() |> List.map (fun d -> fileName, d)) + |> List.ofSeq diff --git a/src/Fable.Transforms/Php/Fable2Php.fs b/src/Fable.Transforms/Php/Fable2Php.fs index 278f7f8c92..e91811f1e3 100644 --- a/src/Fable.Transforms/Php/Fable2Php.fs +++ b/src/Fable.Transforms/Php/Fable2Php.fs @@ -1509,11 +1509,11 @@ and convertValue (com: IPhpCompiler) (value: Fable.ValueKind) range = | RegexMultiline -> "m" | RegexSingleline -> "s" | RegexGlobal -> - addWarning com [] range "Regex global flag is not supported in Php" + WarningCodes.regexFlagUnsupported "global" |> addWarningWithCode com [] range "" | RegexSticky -> - addWarning com [] range "Regex sticky flag is not supported in Php" + WarningCodes.regexFlagUnsupported "sticky" |> addWarningWithCode com [] range "" ) @@ -2138,8 +2138,8 @@ type PhpCompiler(com: Fable.Compiler) = member this.WillPrecompileInlineFunction(file) = com.WillPrecompileInlineFunction(file) - member this.AddLog(msg, severity, rang, fileName, tag) = - com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag) + member this.AddLog(msg, severity, rang, fileName, tag, code) = + com.AddLog(msg, severity, ?range = rang, ?fileName = fileName, ?tag = tag, ?code = code) member this.AddWatchDependency(file) = com.AddWatchDependency(file) member this.AddSignatureDependency(file) = com.AddSignatureDependency(file) diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 477d12facf..2148591fa7 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -340,7 +340,7 @@ let transformReflectionInfo com ctx r (ent: Fable.Entity) generics = let transformTypeTest (com: IPythonCompiler) ctx range expr (typ: Fable.Type) : Expression * Statement list = let warnAndEvalToFalse msg = - "Cannot type test (evals to false): " + msg |> addWarning com [] range + WarningCodes.typeTestEvalsToFalse msg |> addWarningWithCode com [] range Expression.boolConstant false diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 4820be930b..4581313a36 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -562,7 +562,7 @@ let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType: |> Option.map (fun t -> t.Entity.FullName) |> Option.defaultValue "unknown type" - $"Ignoring base call for %s{name}" |> addWarning com [] range + WarningCodes.baseCallIgnored name |> addWarningWithCode com [] range None | Some(Fable.Value _), Some baseType -> @@ -574,8 +574,8 @@ let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType: // Some (baseExpr, []) // default base constructor let range = baseCall |> Option.bind (fun x -> x.Range) - $"Ignoring base call for %s{baseType.Entity.FullName}" - |> addWarning com [] range + WarningCodes.baseCallIgnored baseType.Entity.FullName + |> addWarningWithCode com [] range None | Some _, _ -> diff --git a/src/Fable.Transforms/Python/PythonCompiler.fs b/src/Fable.Transforms/Python/PythonCompiler.fs index 3b1f062230..dff28d2dc0 100644 --- a/src/Fable.Transforms/Python/PythonCompiler.fs +++ b/src/Fable.Transforms/Python/PythonCompiler.fs @@ -138,8 +138,8 @@ type PythonCompiler(com: Compiler) = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = PythonCompiler(com) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 5ef2df57fe..e404a973fb 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1411,8 +1411,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> identityHash com r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - "Types can only be resolved at compile time. At runtime this will be same as `typeof`" - |> addWarning com ctx.InlinePath r + WarningCodes.getTypeOnObj |> addWarningWithCode com ctx.InlinePath r makeTypeInfo r arg.Type |> Some | _ -> None @@ -1538,7 +1537,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "starts_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1550,7 +1549,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "string", "ends_with", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -2348,8 +2347,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2357,10 +2356,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style | ("Parse" | "TryParse") as meth, str :: _ -> @@ -2370,10 +2372,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style @@ -2959,8 +2964,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -2971,7 +2975,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "get_Out" -> Helper.ImportedValue(com, "sys", "stdout", t) |> Some | "get_Error" -> Helper.ImportedValue(com, "sys", "stderr", t) |> Some | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "ReadLine" -> Helper.GlobalCall("input", t, args, ?loc = r) |> Some | "WriteLine" -> log com r t i thisArg args |> Some @@ -3004,7 +3008,7 @@ let stopwatch (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -3023,24 +3027,35 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio IfThenElse(arg, makeDebugger r, unit, r) |> Some | _ -> None -let private ignoreFormatProvider com (ctx: Context) r (moduleName: string) meth args = - match meth, args with - // Ignore IFormatProvider - | "Parse", arg :: _culture :: _styles :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.Parse will ignore culture and styles" +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. Two codes rather than one bundled message so a +/// user who accepts invariant parsing can still be told about a discarded style, and vice versa. +let private ignoreFormatProvider com (ctx: Context) r meth args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles [ arg ] - | "Parse", arg :: _culture :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.Parse will ignore culture" - + | "Parse", arg :: culture :: _ -> + warnProvider culture [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.TryParse will ignore culture and styles" - + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> - addWarning com ctx.InlinePath r $"%s{moduleName}.TryParse will ignore culture" - + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture [ input; defVal ] | _ -> args @@ -3091,7 +3106,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) |> Some | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, "DateOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3148,7 +3163,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> Some | _ -> None | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst Helper.LibCall(com, "TimeOnly", meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3290,8 +3305,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio Helper.LibCall(com, "DateOffset", "compareTo", t, [ thisArg; args.Head ], ?loc = r) ) | "TryParse" -> - let args = - ignoreFormatProvider com ctx r i.DeclaringEntityFullName i.CompiledName args + let args = ignoreFormatProvider com ctx r i.CompiledName args Helper.LibCall(com, moduleName, "tryParse", t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3318,7 +3332,7 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio |> Some | _ -> None | meth -> - let args = ignoreFormatProvider com ctx r i.DeclaringEntityFullName meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 98df0aef12..5f8f6f2f8d 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1117,6 +1117,15 @@ let (|ArrayOrListLiteral|_|) = | MaybeCasted(Value((NewArray(ArrayValues vals, t, _) | ListLiteral(vals, t)), _)) -> ValueSome(vals, t) | _ -> ValueNone +/// `CultureInfo.InvariantCulture`, which every target replaces with an empty object literal (see +/// `globalization`). Matching it keeps "culture is ignored" warnings quiet when the culture asked +/// for is the one Fable uses anyway. +[] +let (|InvariantCulture|_|) = + function + | MaybeCasted(ObjectExpr([], DeclaredType(entRef, _), None)) when entRef.FullName = Types.cultureInfo -> ValueSome() + | _ -> ValueNone + [] let (|IsEntity|_|) fullName = function diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index b934e06f84..c94e9d51b4 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1733,8 +1733,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "GetHashCode", Some arg, _ -> identityHash com r arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then - "Types can only be resolved at compile time. At runtime this will be same as `typeof`" - |> addWarning com ctx.InlinePath r + WarningCodes.getTypeOnObj |> addWarningWithCode com ctx.InlinePath r makeTypeInfo (changeRangeToCallSite ctx.InlinePath r) arg.Type |> Some | _ -> None @@ -1860,7 +1859,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "StartsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "startsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -1870,7 +1869,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) |> Some | "EndsWith", Some c, [ value; ignoreCase; _culture ] -> - addWarning com ctx.InlinePath r "CultureInfo argument is ignored" + WarningCodes.cultureInfoIgnored |> addWarningWithCode com ctx.InlinePath r let args = [ value; ignoreCase ] Helper.LibCall(com, "String", "endsWith", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) @@ -2755,8 +2754,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op && style <> intConst && i.DeclaringEntityFullName <> Types.decimal then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2764,10 +2763,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style |> Some | ("Parse" | "TryParse") as meth, str :: _ -> @@ -2777,10 +2779,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = if i.DeclaringEntityFullName = Types.decimal then @@ -3331,8 +3336,7 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToBase64String" | "FromBase64String" -> if not (List.isSingle args) then - $"Convert.%s{Naming.upperFirst i.CompiledName} only accepts one single argument" - |> addWarning com ctx.InlinePath r + WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) |> Some @@ -3343,7 +3347,7 @@ let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | "get_Out" -> Helper.GlobalIdent("console", "log", t) |> Some | "get_Error" -> Helper.GlobalIdent("console", "error", t) |> Some | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | _ -> None @@ -3358,7 +3362,7 @@ let textWriter (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with | "Write" -> - addWarning com ctx.InlinePath r "Write will behave as WriteLine" + WarningCodes.writeBehavesAsWriteLine |> addWarningWithCode com ctx.InlinePath r log com r t i thisArg args |> Some | "WriteLine" -> log com r t i thisArg args |> Some | "Break" -> makeDebugger r |> Some @@ -3375,12 +3379,37 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio IfThenElse(cond, makeDebugger r, unit, r) |> Some | _ -> None -let ignoreFormatProvider meth args = +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. `Parse arg` with no extra argument discards +/// nothing, so it must not warn. +let ignoreFormatProvider com (ctx: Context) r meth args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match meth, args with - // Ignore IFormatProvider + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture + [ input; defVal ] | _ -> args let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3455,7 +3484,7 @@ let dateTime (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3566,7 +3595,7 @@ let dateTimeOffset (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: E getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3629,7 +3658,7 @@ let dateOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op getFieldWith r t thisArg.Value fieldName |> Some | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3639,7 +3668,7 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg let moduleName = timeSpanModule com let timeSpanLibCall meth args = - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix meth |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -3653,11 +3682,7 @@ let timeSpans (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg let limitArgsCountToMilliseconds (meth: string) (maxArgsCount: int) = if args.Length > maxArgsCount then if isNotDefaultInt64ZeroValue args.[maxArgsCount] then - addWarning - com - ctx.InlinePath - r - "TimeSpan precision is limited to milliseconds, microsecond arguments will be ignored" + WarningCodes.timeSpanPrecisionIgnored |> addWarningWithCode com ctx.InlinePath r args |> List.take maxArgsCount |> timeSpanLibCall meth @@ -3789,7 +3814,7 @@ let timeOnly (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> Some | _ -> None | meth -> - let args = ignoreFormatProvider meth args + let args = ignoreFormatProvider com ctx r meth args let meth = Naming.removeGetSetPrefix i.CompiledName |> Naming.lowerFirst Helper.LibCall(com, moduleName, meth, t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) @@ -4244,7 +4269,8 @@ let asyncBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r Helper.LibCall(com, "Async", "start", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some @@ -4316,8 +4342,7 @@ let tasks (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio match thisArg, i.CompiledName with | Some x, "GetAwaiter" -> Some x | Some x, ("GetResult" | "get_Result" | "Result") -> - "Task.Result and GetAwaiter().GetResult() return the task itself in JS/TS, as there's no way to block until it completes. Use let! in task { } or Async.AwaitTask instead." - |> addWarning com ctx.InlinePath r + WarningCodes.taskResultReturnsTask |> addWarningWithCode com ctx.InlinePath r Some x | None, "Delay" -> @@ -4663,6 +4688,10 @@ let fsharpType com (ctx: Context) methName (r: SourceLocation option) t (i: Call // Prevent name clash with FSharpValue.GetRecordFields | "GetRecordFields" -> // Drop the trailing `allowAccessToPrivateRepresentation` flag (no meaning in JS/TS) + if List.length args > 1 then + WarningCodes.privateRepresentationFlagIgnored + |> addWarningWithCode com ctx.InlinePath r + Helper.LibCall(com, "Reflection", "getRecordElements", t, List.truncate 1 args, i.SignatureArgTypes, ?loc = r) |> Some | "GetUnionCases" @@ -4673,8 +4702,8 @@ let fsharpType com (ctx: Context) methName (r: SourceLocation option) t (i: Call | "IsTuple" | "IsFunction" -> if List.length args > 1 then - $"FSharpType.%s{methName}(): second argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.privateRepresentationFlagIgnored + |> addWarningWithCode com ctx.InlinePath r let args = [ List.head args ] diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 4bcf1bbe0c..1dbcb27e8a 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -4896,9 +4896,7 @@ module Util = let transformModuleAction (com: IRustCompiler) ctx (body: Fable.Expr) = // optional, uses startup::on_startup! for static execution (before main). // See also: https://doc.rust-lang.org/1.6.0/complement-design-faq.html#there-is-no-life-before-or-after-main-no-static-ctorsdtors - "For Rust, support for F# static and module do bindings is disabled by default. " - + "It can be enabled with the 'static_do_bindings' feature. Use at your own risk!" - |> addWarning com [] body.Range + WarningCodes.staticDoBindingsDisabled |> addWarningWithCode com [] body.Range let expr = transformExpr com ctx body let attrs = [] //[mkAttr "cfg" ["feature = \"static_do_bindings\""]] @@ -6441,8 +6439,8 @@ module Compiler = member _.AddWatchDependency(fileName) = com.AddWatchDependency(fileName) member _.AddSignatureDependency(fileName) = com.AddSignatureDependency(fileName) - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag) + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + com.AddLog(msg, severity, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) let makeCompiler com = RustCompiler(com) diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index c242b41b0d..91add9cd16 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -1338,8 +1338,8 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | "GetTypeCode", Some c, [] -> makeLibCall com r t i "Char" "GetTypeCode" [ c ] |> Some | ("TryParse" | "Parse") as meth, None, _ -> makeLibCall com r t i "Char" meth args |> Some | ("IsSurrogate" | "IsHighSurrogate" | "IsLowSurrogate" | "IsSurrogatePair") as meth, None, _ -> - $"Rust chars are Unicode scalar values, so surrogate tests will be false." - |> addWarning com ctx.InlinePath r + WarningCodes.charSurrogateTestsAreFalse + |> addWarningWithCode com ctx.InlinePath r let meth = getMethodName meth args @@ -1403,13 +1403,13 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt ExprType(Number(Int32, _)) ] -> libCall com r t "String" "compareOrdinal2" args |> Some | _ -> None | "CompareTo", Some c, [ ExprTypeAs(String, arg) ] -> - $"String.CompareTo will be compiled as String.CompareOrdinal" - |> addWarning com ctx.InlinePath r + WarningCodes.stringCompareIsOrdinal "CompareTo" + |> addWarningWithCode com ctx.InlinePath r libCall com r t "String" "compareOrdinal" [ c; arg ] |> Some | "Compare", None, _ -> - $"String.Compare will be compiled as String.CompareOrdinal" - |> addWarning com ctx.InlinePath r + WarningCodes.stringCompareIsOrdinal "Compare" + |> addWarningWithCode com ctx.InlinePath r match args with | [ ExprType String; ExprType String ] -> libCall com r t "String" "compareOrdinal" args |> Some @@ -1464,8 +1464,7 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt match args with | (ExprType String :: _) -> "sprintf!" |> emitFormat com r t args |> Some | (cultureInfo :: restArgs) -> - $"String.Format(): Format provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r "sprintf!" |> emitFormat com r t restArgs |> Some | _ -> None @@ -1647,8 +1646,7 @@ let stringBuilder (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Ex libCall com r t "Util" "sb_Append" [ sb; s ] |> Some | (cultureInfo :: restArgs) -> - $"StringBuilder.AppendFormat(): Format provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let s = "sprintf!" |> emitFormat com None String restArgs @@ -2072,8 +2070,8 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op let intConst = int System.Globalization.NumberStyles.Integer if style <> hexConst && style <> intConst then - $"%s{i.DeclaringEntityFullName}.%s{meth}(): NumberStyle %d{style} is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.numberStylesIgnored style + |> addWarningWithCode com ctx.InlinePath r let acceptedArgs = if meth = "Parse" then @@ -2081,10 +2079,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 3 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, style, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r parseCall meth str args style |> Some | ("Parse" | "TryParse") as meth, _, str :: _ -> @@ -2094,10 +2095,13 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else 2 - if List.length args > acceptedArgs then + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> // e.g. Double.Parse(string, IFormatProvider) etc. - $"%s{i.DeclaringEntityFullName}.%s{meth}(): provider argument is ignored" - |> addWarning com ctx.InlinePath r + WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r let style = int System.Globalization.NumberStyles.Any parseCall meth str args style |> Some @@ -2498,19 +2502,44 @@ let debug (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | _ -> None | _ -> None -let ignoreFormatProvider compiledName args = +/// Drops the `IFormatProvider` (and `DateTimeStyles`, where present) a date/time `Parse` was +/// given, warning once per discarded argument. `Parse arg` with no extra argument discards +/// nothing, so it must not warn. `ToString` stays silent here, as it does on every other target. +let ignoreFormatProvider com (ctx: Context) r compiledName args = + // Passing InvariantCulture asks for exactly what Fable does, so there is nothing to report. + let warnProvider culture = + match culture with + | InvariantCulture -> () + | _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + + let warnStyles styles = + match styles with + | NumberConst(NumberValue.Int32 0, _) -> () // DateTimeStyles.None: no special handling + | _ -> WarningCodes.dateTimeStylesIgnored |> addWarningWithCode com ctx.InlinePath r + match compiledName, args with - // Ignore IFormatProvider | "ToString", ExprTypeAs(String, arg) :: _ -> [ arg ] | "ToString", _ -> [ makeStrConst "" ] // default (no format string) + | "Parse", arg :: culture :: styles :: _ -> + warnProvider culture + warnStyles styles + [ arg ] + | "Parse", arg :: culture :: _ -> + warnProvider culture + [ arg ] | "Parse", arg :: _ -> [ arg ] - | "TryParse", input :: _culture :: _styles :: defVal :: _ -> [ input; defVal ] - | "TryParse", input :: _culture :: defVal :: _ -> [ input; defVal ] + | "TryParse", input :: culture :: styles :: defVal :: _ -> + warnProvider culture + warnStyles styles + [ input; defVal ] + | "TryParse", input :: culture :: defVal :: _ -> + warnProvider culture + [ input; defVal ] | _ -> args let makeMemberCall com ctx r t i moduleName memberName (thisArg: Expr option) (args: Expr list) = let memberName = Naming.removeGetSetPrefix memberName |> Naming.lowerFirst - let args = ignoreFormatProvider i.CompiledName args + let args = ignoreFormatProvider com ctx r i.CompiledName args match thisArg with | Some c -> makeInstanceCall r t i c memberName args @@ -3064,7 +3093,8 @@ let asyncs com (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr lis match i.CompiledName with // TODO: Throw error for RunSynchronously | "Start" -> - "Async.Start will behave as StartImmediate" |> addWarning com ctx.InlinePath r + WarningCodes.asyncStartBehavesAsStartImmediate + |> addWarningWithCode com ctx.InlinePath r makeLibCall com r t i "Async" "start" args |> Some // Make sure cancellationToken is called as a function and not a getter diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 496ddd55cf..3d6ee95c31 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -452,15 +452,17 @@ type LogEntry = Severity: Severity Range: SourceLocation option FileName: string option + Code: string option } - static member Make(severity, msg, ?fileName, ?range, ?tag) = + static member Make(severity, msg, ?fileName, ?range, ?tag, ?code) = { Message = msg Tag = defaultArg tag "FABLE" Severity = severity Range = range FileName = fileName + Code = code } static member MakeError(msg, ?fileName, ?range, ?tag) = @@ -479,7 +481,8 @@ type CompilerImpl ?watchDependencies: HashSet, ?signatureDependencies: HashSet, ?logs: ResizeArray, - ?isPrecompilingInlineFunction: bool + ?isPrecompilingInlineFunction: bool, + ?warningSuppression: WarningSuppression.Resolver ) = @@ -488,6 +491,11 @@ type CompilerImpl let logs = Option.defaultWith ResizeArray logs let fableLibraryDir = fableLibDir.TrimEnd('/') + let getSuppressions fileName = + match warningSuppression with + | Some resolver -> resolver.For(fileName) + | None -> WarningSuppression.FileSuppressions.Empty + member _.Logs = logs.ToArray() member _.WatchDependencies = @@ -535,7 +543,8 @@ type CompilerImpl ?watchDependencies = watchDependencies, ?signatureDependencies = signatureDependencies, logs = logs, - isPrecompilingInlineFunction = true + isPrecompilingInlineFunction = true, + ?warningSuppression = warningSuppression ) member _.GetImplementationFile(fileName) = @@ -599,6 +608,29 @@ type CompilerImpl | Some signatureDependencies when file <> currentFile -> signatureDependencies.Add(file) |> ignore | _ -> () - member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string) = - LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag) - |> logs.Add + member _.AddLog(msg, severity, ?range, ?fileName: string, ?tag: string, ?code: string) = + // Only warnings can be suppressed, errors always surface (matches F#'s own #nowarn), + // and neither can the warnings about the `fable-disable` directives themselves. + let isSuppressed = + severity = Severity.Warning + && WarningCodes.isSuppressible code + && ( + match range with + | None -> false + | Some(r: SourceLocation) -> + let file = + match fileName with + | Some f -> f + | None -> + match r.File with + | Some f -> f + | None -> currentFile + + // The whole range, not just its first line, so a trailing + // `// fable-disable-line` still works on a multi-line expression. + (getSuppressions file).IsSuppressed(r.start.line, r.``end``.line, code) + ) + + if not isSuppressed then + LogEntry.Make(severity, msg, ?range = range, ?fileName = fileName, ?tag = tag, ?code = code) + |> logs.Add diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index bbe89065d1..5fa5235db0 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -234,6 +234,9 @@ module Types = [] let rune = "System.Text.Rune" + [] + let cultureInfo = "System.Globalization.CultureInfo" + [] let timer = "System.Timers.Timer" @@ -477,9 +480,6 @@ module Types = [] let iformatProvider = "System.IFormatProvider" - [] - let cultureInfo = "System.Globalization.CultureInfo" - [] let iobserverGeneric = "System.IObserver`1" @@ -680,7 +680,14 @@ module Log = FromRange: SourceLocation option } - let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + let private addLogWithCode + (com: Compiler) + (inlinePath: InlinePath list) + (range: SourceLocation option) + msg + severity + (code: string option) + = let printInlineSource fromPath (p: InlinePath) = let path = Path.getRelativeFileOrDirPath false fromPath false p.FromFile @@ -697,11 +704,22 @@ module Log = file, msg + " - Inline call from " + inlinePath | [] -> range |> Option.bind (fun r -> r.File) |> Option.defaultValue com.CurrentFile, msg - com.AddLog(msg, severity, ?range = range, fileName = actualFile) + com.AddLog(msg, severity, ?range = range, fileName = actualFile, ?code = code) + + let private addLog (com: Compiler) (inlinePath: InlinePath list) (range: SourceLocation option) msg severity = + addLogWithCode com inlinePath range msg severity None let addWarning (com: Compiler) inlinePath range warning = addLog com inlinePath range warning Severity.Warning + /// Same as `addWarning`, but tags the warning with a stable code (e.g. "FABLE0001") so it + /// can be suppressed via `// fable-disable-line/-next-line/-enable CODE` comments. + /// Meant to be used as `WarningCodes.someWarning arg1 arg2 |> addWarningWithCode com inlinePath range`. + /// + /// Note that a warning coming out of an inlined function is attributed to the file that *defines* it + let addWarningWithCode (com: Compiler) inlinePath range ((code, warning): string * string) = + addLogWithCode com inlinePath range warning Severity.Warning (Some code) + let addError (com: Compiler) inlinePath range error = addLog com inlinePath range error Severity.Error diff --git a/src/fable-standalone/src/Fable.Standalone.fsproj b/src/fable-standalone/src/Fable.Standalone.fsproj index 97dac79b02..48622595c6 100644 --- a/src/fable-standalone/src/Fable.Standalone.fsproj +++ b/src/fable-standalone/src/Fable.Standalone.fsproj @@ -25,6 +25,8 @@ + + diff --git a/src/fable-standalone/src/Main.fs b/src/fable-standalone/src/Main.fs index b69ae24298..53a5d0229a 100644 --- a/src/fable-standalone/src/Main.fs +++ b/src/fable-standalone/src/Main.fs @@ -161,6 +161,7 @@ let makeCompiler fableLibrary typedArrays language fsharpOptions project fileNam ?typedArrays = typedArrays ) + // No `warningSuppression`: the REPL has no `SourceReader` to scan, so `// fable-disable` comments are a no-op here. CompilerImpl(fileName, project, options, fableLibrary) /// `fable precompile` sorts inline expressions by member name, splits them into chunks and records diff --git a/tests/Integration/Compiler/BehaviorWarningTests.fs b/tests/Integration/Compiler/BehaviorWarningTests.fs new file mode 100644 index 0000000000..642f6a1234 --- /dev/null +++ b/tests/Integration/Compiler/BehaviorWarningTests.fs @@ -0,0 +1,117 @@ +module Fable.Tests.Compiler.BehaviorWarning + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +(* + Warnings about code that compiles but behaves differently from .NET. Each one carries a + code so that a user who accepts the difference can silence it at the call site. +*) + +let tests = + testList "Behavior Warnings" [ + testCase "Console.Write is reported" <| fun _ -> + let source = + """ +System.Console.Write("x") +""" + compile source + |> Assert.Code.warning "FABLE0108" + |> ignore + + testCase "Console.Write can be suppressed by code" <| fun _ -> + let source = + """ +System.Console.Write("x") // fable-disable-line FABLE0108 +""" + compile source + |> Assert.Code.noWarning "FABLE0108" + |> ignore + + testCase "Async.Start is reported" <| fun _ -> + let source = + """ +Async.Start(async { () }) +""" + compile source + |> Assert.Code.warning "FABLE0109" + |> ignore + + testCase "Task.Result is reported" <| fun _ -> + let source = + """ +let t = task { return 1 } +let r: int = t.Result +""" + compile source + |> Assert.Code.warning "FABLE0110" + |> ignore + + testCase "GetType on obj is reported" <| fun _ -> + let source = + """ +let x: obj = box 1 +x.GetType() |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0111" + |> ignore + + testCase "A type test against an interface is reported" <| fun _ -> + let source = + """ +type IFoo = + abstract X: int + +let isFoo (o: obj) = match o with :? IFoo -> true | _ -> false +""" + compile source + |> Assert.Code.warning "FABLE0112" + |> ignore + + testCase "Inheriting through a secondary constructor is reported" <| fun _ -> + let source = + """ +type Base = + val X: int + new() = { X = 1 } + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Code.warning "FABLE0114" + |> ignore + + testCase "An inlined argument at risk of double evaluation is reported" <| fun _ -> + let source = + """ +let inline orZero (o: int option) = + defaultArg o 0 + (if o.IsSome then 1 else 0) + +orZero (Some(System.Random().Next())) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0115" + |> ignore + + testCase "CaseTest outside a TypeScriptTaggedUnion is reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Id = + | [] Name of string + | [] Slug of string + +let isName (id: Id) = match id with Name _ -> true | Slug _ -> false +""" + compile source + |> Assert.Code.warning "FABLE0116" + |> ignore + ] diff --git a/tests/Integration/Compiler/CompilerHelpersTests.fs b/tests/Integration/Compiler/CompilerHelpersTests.fs index 5b4bd5dfa7..a3a2bfa393 100644 --- a/tests/Integration/Compiler/CompilerHelpersTests.fs +++ b/tests/Integration/Compiler/CompilerHelpersTests.fs @@ -5,8 +5,69 @@ open Util.Testing open Fable.Tests.Compiler.Util open Fable.Tests.Compiler.Util.Compiler +open System +open System.Reflection +open Fable.Transforms + +/// Every warning code `WarningCodes` can emit, read from the module itself so that this test +/// has no list of its own to fall out of date. +let private codesTheRegistryCanProduce () = + let flags = BindingFlags.Public ||| BindingFlags.Static + let moduleType = typeof.Assembly.GetType("Fable.Transforms.WarningCodes") + let codeOf (pair: obj) = fst (pair :?> string * string) + + // The parameterised warnings interpolate their argument into the message, never into the + // code, so anything of the right type will do. + let throwaway (p: ParameterInfo) = + if p.ParameterType = typeof then box "" + elif p.ParameterType.IsValueType then Activator.CreateInstance p.ParameterType + else null + + // A warning is anything shaped `string * string`: plain ones compile to static properties, + // parameterised ones to static methods. Nothing else in the module has that shape. + let fromValues = + moduleType.GetProperties(flags) + |> Array.filter (fun p -> p.PropertyType = typeof) + |> Array.map (fun p -> codeOf (p.GetValue(null))) + + let fromFactories = + moduleType.GetMethods(flags) + |> Array.filter (fun m -> not m.IsSpecialName && m.ReturnType = typeof) + |> Array.map (fun m -> codeOf (m.Invoke(null, m.GetParameters() |> Array.map throwaway))) + + Set.ofArray (Array.append fromValues fromFactories) + let tests = testList "Compiler Helpers" [ + // The goal of this test is to act as a safe guard for us, so we remember to add + // warning code to the knownCodes list. + testCase "knownCodes lists every warning the registry can produce" <| fun _ -> + // Forgetting to add a code is otherwise silent until somebody writes a directive for + // it and gets told it's a typo - the one thing knownCodes exists to prevent. + let produced = codesTheRegistryCanProduce () + + // Guards against the reflection above quietly finding nothing, which would make every + // assertion below pass for the wrong reason. + if Set.isEmpty produced then + failwith "Found no warnings in WarningCodes - the reflection in this test has gone stale" + + let missing = Set.difference produced WarningCodes.knownCodes + let stale = Set.difference WarningCodes.knownCodes produced + + if not (Set.isEmpty missing) || not (Set.isEmpty stale) then + failwithf "knownCodes is out of sync. Missing: %A. Listed but unused: %A" missing stale + + testCase "isSuppressible splits the code bands at FABLE0100" <| fun _ -> + // Directive warnings sit below the boundary and can never be silenced by a directive. + WarningCodes.isSuppressible (Some "FABLE0001") |> equal false + WarningCodes.isSuppressible (Some "FABLE0002") |> equal false + WarningCodes.isSuppressible (Some "FABLE0003") |> equal false + WarningCodes.isSuppressible (Some "FABLE0099") |> equal false + WarningCodes.isSuppressible (Some "FABLE0100") |> equal true + WarningCodes.isSuppressible (Some "FABLE9999") |> equal true + // A warning with no code at all is still caught by a bare `// fable-disable-line`. + WarningCodes.isSuppressible None |> equal true + testCase "expectedVersionMatchesActual works for same major version" <| fun _ -> Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.0" |> equal true Fable.CompilerExt.expectedVersionMatchesActual "5.0.0" "5.0.1" |> equal true diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index e686023c12..b55fd2a616 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -20,6 +20,7 @@ let tests = compile source |> Assert.Is.success |> ignore + testCase "Compile printfn" <| fun _ -> let source = "printfn \"Hello %s\" \"World\"" compile source @@ -31,6 +32,7 @@ let tests = compile source |> Assert.Is.Single.error |> ignore + testCase "Compiling incomplete pattern match results in warning" <| fun _ -> let source = "match None with | Some n -> 42 |> ignore" // without `ignore`: Warning: Result of Expression is implicitly ignored compile source @@ -42,6 +44,7 @@ let tests = compile source |> Assert.Exists.errorWith "This expression was expected to have type" |> ignore + testCase "Compiling incomplete pattern match results in specific warning" <| fun _ -> let source = "match None with | Some n -> 42" compile source @@ -330,4 +333,20 @@ let isOptions (o: obj) = match o with :? Options -> true | _ -> false compile source |> Assert.Exists.warningWith "Cannot type test (evals to false): Program.Options (pojo)" |> ignore + + testCase "The formatted output carries the warning code" <| fun _ -> + // Without this there is no way to discover which code to put in a directive. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + let formatted = + compile source + |> List.filter (fun log -> log.Code = Some "FABLE0100") + |> List.map (Fable.Cli.Main.Util.formatLog Compiler.Cached.projDir) + + match formatted with + | [] -> failwith "Expected a FABLE0100 warning" + | messages -> equal true (messages |> List.forall (fun m -> m.Contains "warning FABLE FABLE0100:")) ] diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index 8a2b09954d..a7134d6375 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -24,6 +24,9 @@ + + + diff --git a/tests/Integration/Compiler/IgnoredArgumentTests.fs b/tests/Integration/Compiler/IgnoredArgumentTests.fs new file mode 100644 index 0000000000..a2af15784f --- /dev/null +++ b/tests/Integration/Compiler/IgnoredArgumentTests.fs @@ -0,0 +1,203 @@ +module Fable.Tests.Compiler.IgnoredArgument + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +(* + Verify that warnings are generated when needed, and skipped in the scenario where Fable + has the same behavior as .NET. + + The later makes the logs less verbose and warnings more meaningful +*) + +let tests = + testList "Ignored Arguments" [ + testCase "Discarding a format provider on a date parse is reported" <| fun _ -> + // JS, Dart and Rust used to drop this argument silently while Python warned. Aligning them + // is a behaviour change: code that compiled quietly now raises FABLE0102. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.GetCultureInfo "fr-FR") |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> ignore + + testCase "Passing InvariantCulture to a parse is not reported" <| fun _ -> + // Fable parses with a fixed culture-independent implementation, so InvariantCulture asks + // for exactly what it gets. Warning here would fire on every correct call - and does, if + // the exemption is removed: 96 sites across the repo's own test suites. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "A discarded DateTimeStyles is reported even when the culture is exempt" <| fun _ -> + // Two codes rather than one bundled message: the style is still discarded regardless of + // which culture was passed, so exempting the culture must not silence it. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0104" + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "DateTimeStyles.None is not reported" <| fun _ -> + // `None` means "no special handling", which is what Fable does - same exemption as + // InvariantCulture. Every one of the 12 sites in tests/Js passes exactly this. + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.InvariantCulture, DateTimeStyles.None) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> Assert.Code.noWarning "FABLE0104" + |> ignore + + testCase "A numeric parse given InvariantCulture is not reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("10.5", CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> ignore + + testCase "A numeric parse given a real culture is reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("10.5", CultureInfo.GetCultureInfo "fr-FR") |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> ignore + + testCase "A real culture and a style are both reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +DateTime.Parse("2026-01-01", CultureInfo.GetCultureInfo "fr-FR", DateTimeStyles.AssumeUniversal) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0102" + |> Assert.Code.warning "FABLE0104" + |> ignore + + testCase "A date parse with no extra argument discards nothing and is silent" <| fun _ -> + let source = + """ +open System +DateTime.Parse("2026-01-01") |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0102" + |> Assert.Code.noWarning "FABLE0104" + |> ignore + + testCase "A discarded NumberStyles is reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("1.5", NumberStyles.Currency, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0103" + |> ignore + + testCase "A TimeSpan built with microseconds is reported" <| fun _ -> + // The runtime representation only carries milliseconds, so the finer component is dropped. + let source = + """ +open System +TimeSpan.FromMilliseconds(1L, 500L) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0105" + |> ignore + + testCase "A TimeSpan with no microseconds is silent" <| fun _ -> + let source = + """ +open System +TimeSpan.FromMilliseconds(1L, 0L) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0105" + |> ignore + + testCase "A private-representation flag on FSharpType is reported" <| fun _ -> + let source = + """ +open Microsoft.FSharp.Reflection +FSharpType.IsUnion(typeof, true) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0106" + |> ignore + + testCase "A private-representation flag on GetRecordFields is reported" <| fun _ -> + // This one dropped the flag silently while its seven siblings warned, all in one function. + let source = + """ +open Microsoft.FSharp.Reflection +type R = { A: int } +FSharpType.GetRecordFields(typeof, true) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0106" + |> ignore + + testCase "FSharpType without the flag is silent" <| fun _ -> + let source = + """ +open Microsoft.FSharp.Reflection +FSharpType.IsUnion(typeof) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0106" + |> ignore + + testCase "Base64 offset and length arguments are reported" <| fun _ -> + // The slice is silently encoded whole, so this is a wrong value rather than a formatting nit. + let source = + """ +open System +Convert.ToBase64String([| 1uy; 2uy; 3uy; 4uy |], 0, 2) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0107" + |> ignore + + testCase "Base64 with only the array is silent" <| fun _ -> + let source = + """ +open System +Convert.ToBase64String([| 1uy; 2uy; 3uy; 4uy |]) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0107" + |> ignore + ] diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index d8fddd671f..7c1909fe97 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -7,6 +7,9 @@ let allTests = [ CompilerMessages.tests CodeServicesApi.tests + WarningSuppression.tests + IgnoredArgument.tests + BehaviorWarning.tests AnonRecordInInterface.tests CompilerHelpers.tests Inflate.tests diff --git a/tests/Integration/Compiler/Util/Compiler.fs b/tests/Integration/Compiler/Util/Compiler.fs index 45ca9148dc..70e7684c09 100644 --- a/tests/Integration/Compiler/Util/Compiler.fs +++ b/tests/Integration/Compiler/Util/Compiler.fs @@ -32,7 +32,9 @@ module Compiler = let projFile = IO.Path.Join(projDir, name + ".fsproj") |> Path.normalizeFullPath let cliArgs = - let compilerOptions = CompilerOptionsHelper.Make() + // The real CLI always defines these (Entry.fs), and code guarded by them is exactly where + // Fable warnings - and the comments suppressing them - live. + let compilerOptions = CompilerOptionsHelper.Make(define = ["FABLE_COMPILER"; "FABLE_COMPILER_JAVASCRIPT"]) { CliArgs.ProjectFile = projFile FableLibraryPath = None RootDir = projDir @@ -56,6 +58,8 @@ module Compiler = let mutable state = State.Create(cliArgs, recompileAllFiles=true) + member _.ProjDir = projDir + member _.Compile(files: (string * string) list) = for (fileName, content) in files do IO.File.WriteAllText(IO.Path.Join(projDir, fileName), content) @@ -74,6 +78,8 @@ module Compiler = let private project = CachedProject("TestProject") let private projectWithSignature = CachedProject("TestProjectSignature") + let projDir = project.ProjDir + let private emptyLibrary = "module Library" + Environment.NewLine let private programFile settings source = @@ -164,6 +170,8 @@ module Compiler = let errorOrWarning: Result -> bool = List.isEmpty >> not let matching = List.exists let withMsg (txt: string) = List.exists (fun m -> m.Message.Contains txt) + module Code = + let is (code: string) (msg: LogEntry) = msg.Code = Some code module Text = let contains (txt: string) msg = msg.Message.Contains(txt, StringComparison.InvariantCultureIgnoreCase) let isMatch (regex: System.Text.RegularExpressions.Regex) (msg: LogEntry) = @@ -228,6 +236,16 @@ module Compiler = Test.All.warning |> orFail expected.All.Warning + /// Assertions on the stable `FABLExxxx` codes rather than on message text: what a + /// `// fable-disable` comment targets is the code, so that's what the tests must pin. + module Code = + let warning (code: string) = + Test.Exists.matching (Test.Is.warning >&> Test.Code.is code) + |> orFail (expected.Warning.With code) + let noWarning (code: string) = + (not << Test.Exists.matching (Test.Is.warning >&> Test.Code.is code)) + |> orFail (expected.No.Warning.With code) + module Exists = let errorWith (txt: string) = Test.Exists.matching (Test.Is.error >&> Test.Text.contains txt) diff --git a/tests/Integration/Compiler/WarningSuppressionTests.fs b/tests/Integration/Compiler/WarningSuppressionTests.fs new file mode 100644 index 0000000000..bda6ca3d3b --- /dev/null +++ b/tests/Integration/Compiler/WarningSuppressionTests.fs @@ -0,0 +1,322 @@ +module Fable.Tests.Compiler.WarningSuppression + +open Fable.Core +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +/// Exercises the `// fable-disable` machinery itself - directive parsing, block scoping, which +/// warnings it can reach, and the FABLE0001-band reports about the directives. FABLE0100 stands +/// in as the subject warning throughout; what is under test is the mechanism, not that code. +let tests = + testList "Warning Suppression" [ + testCase "CultureInfo argument warning is not suppressed by default" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "The same code covers both StartsWith and EndsWith call sites" <| fun _ -> + // StartsWith and EndsWith raise the same logical "CultureInfo argument is ignored" warning + // from two separate call sites in Replacements.fs, sharing WarningCodes.CultureInfoIgnored. + // One code must suppress both, otherwise the registry has failed at its only job. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-line suppresses a warning on the same line" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable-next-line suppresses a warning on the following line" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "fable-disable/fable-enable suppresses warnings in a block" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore +// fable-enable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Are.warnings 1 + |> ignore + + testCase "A mismatched code does not suppress the warning" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line SOME_OTHER_CODE +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "A bare fable-disable-line suppresses regardless of code" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line +""" + compile source + |> Assert.Are.warnings 0 + |> ignore + + testCase "A string literal that looks like a directive is not treated as one" <| fun _ -> + let source = + """ +open System.Globalization +let s = "// fable-disable-line FABLE0100" +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Exists.warningWith "CultureInfo argument is ignored" + |> ignore + + testCase "A directive inside a #if FABLE_COMPILER block is honoured" <| fun _ -> + let source = + """ +open System.Globalization +#if FABLE_COMPILER +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +#endif +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A warning inside a #if FABLE_COMPILER block still fires without a directive" <| fun _ -> + // Guards the test above from passing vacuously because the block was compiled out. + let source = + """ +open System.Globalization +#if FABLE_COMPILER +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +#endif +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + + testCase "Errors are never suppressed, not even by a blanket fable-disable" <| fun _ -> + let source = + """ +open Fable.Core.JsInterop + +type Response = + abstract fn: int -> int + abstract prop: bool with get, set + +// fable-disable +let res = jsOptions (fun o -> o.fn <- (fun i -> i)) +""" + compile source + |> Assert.Exists.errorWith "Cannot set a non-property member in 'jsOptions'" + |> ignore + + testCase "A directive written as a block comment is honoured" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore (* fable-disable-line FABLE0100 *) +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A trailing directive suppresses a warning spanning several lines" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith( + "a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A fable-disable block with no fable-enable runs to the end of the file" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable FABLE0100 +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +"abc".EndsWith("c", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A colon separator and a lower-case code are accepted" <| fun _ -> + // What people coming from `# noqa: E501` and `@ts-ignore` will write. + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line: fable0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A justification after -- is not parsed as codes" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 -- culture is irrelevant here +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> Assert.Code.noWarning "FABLE0001" + |> ignore + + testCase "A typo'd code is reported instead of silently suppressing nothing" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABEL0001 +""" + compile source + |> Assert.Code.warning "FABLE0001" + |> Assert.Code.warning "FABLE0100" + // The typo report is the actionable one; don't pile "and it's unused" on top of it. + |> Assert.Code.noWarning "FABLE0002" + |> ignore + + testCase "A directive that suppresses nothing is reported as unused" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0100 +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0002" + |> ignore + + testCase "A directive that does its job is not reported as unused" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.noWarning "FABLE0002" + |> ignore + + testCase "A fable-disable block with no codes is reported" <| fun _ -> + // It would otherwise silence every Fable warning to the end of the file, unnoticed. + let source = + """ +open System.Globalization +// fable-disable +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.warning "FABLE0003" + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "Directive warnings cannot be suppressed by a directive" <| fun _ -> + // FABLE0001-0099 flag comment text that is wrong to keep, so the only correct answer is to + // edit it. Silencing "this directive is broken" with another directive is self-defeating. + let source = + """ +open System.Globalization +// fable-disable-next-line FABLE0002 +// fable-disable-next-line FABLE0100 +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0002" + |> ignore + + testCase "A blanket fable-disable cannot suppress its own report" <| fun _ -> + let source = + """ +open System.Globalization +// fable-disable +let answer = 42 +""" + compile source + |> Assert.Code.warning "FABLE0003" + |> ignore + + testCase "A word merely starting with a directive name is not a directive" <| fun _ -> + let source = + """ +open System.Globalization +"abc".StartsWith("a", true, CultureInfo.InvariantCulture) |> ignore // fable-disabled for now +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + + testCase "Directives are found in CRLF sources" <| fun _ -> + let source = + [ "" + "open System.Globalization" + "\"abc\".StartsWith(\"a\", true, CultureInfo.InvariantCulture) |> ignore // fable-disable-line FABLE0100" + "" ] + |> String.concat "\r\n" + + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A warning from an inlined function is suppressed at its definition" <| fun _ -> + // The warning is attributed to the file the inline function is *defined* in, so that is + // where the directive has to go - the call site can't suppress it. + let source = + """ +open System.Globalization +let inline startsWithCulture (s: string) = + s.StartsWith("a", true, CultureInfo.InvariantCulture) // fable-disable-line FABLE0100 + +startsWithCulture "abc" |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0100" + |> ignore + + testCase "A directive at the call site does not suppress an inlined function's warning" <| fun _ -> + let source = + """ +open System.Globalization +let inline startsWithCulture (s: string) = + s.StartsWith("a", true, CultureInfo.InvariantCulture) + +startsWithCulture "abc" |> ignore // fable-disable-line FABLE0100 +""" + compile source + |> Assert.Code.warning "FABLE0100" + |> ignore + ] From 09158b05acf4c35b54111f4f1d72b96930bce18e Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 18:02:01 +0200 Subject: [PATCH 216/322] fix(all): kill --run process on SIGTERM everywhere (#5140) --- src/Fable.Compiler/Util.fs | 56 ++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/Fable.Compiler/Util.fs b/src/Fable.Compiler/Util.fs index 27ced7521a..dbc1d9d8fd 100644 --- a/src/Fable.Compiler/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -438,24 +438,54 @@ module Process = if not p.HasExited then p.Kill(entireProcessTree = true) - let startWithEnv envVars = - let mutable runningProcess = None + let mutable private runProcess: Process option = None + + let private killRunProcess () = + runProcess + |> Option.iter (fun p -> + try + kill p + with _ -> + () + ) + + // Terminating Fable doesn't terminate the processes it spawned, so kill them when Fable exits. + // SIGTERM is what IDEs and task runners send, and the runtime only intercepts it when registered. + let private killRunProcessOnExit = + lazy + (AppDomain.CurrentDomain.ProcessExit.Add(fun _ -> killRunProcess ()) + + if isWindows () then + Console.CancelKeyPress.AddHandler(ConsoleCancelEventHandler(fun _ _ -> killRunProcess ())) - // In Windows, terminating the main process doesn't kill the spawned ones so we need - // to listen for the Console.CancelKeyPress and AssemblyLoadContext.Unloading events - if isWindows () then - Console.CancelKeyPress.AddHandler(ConsoleCancelEventHandler(fun _ _ -> runningProcess |> Option.iter kill)) + let assemblyLoadContext = + getCurrentAssembly () |> Loader.AssemblyLoadContext.GetLoadContext - let assemblyLoadContext = - getCurrentAssembly () |> Loader.AssemblyLoadContext.GetLoadContext + assemblyLoadContext.add_Unloading (fun _ -> killRunProcess ()) - assemblyLoadContext.add_Unloading (fun _ -> runningProcess |> Option.iter kill) + Runtime.InteropServices.PosixSignalRegistration.Create( + Runtime.InteropServices.PosixSignal.SIGTERM, + (fun _ -> killRunProcess ()) + )) + let private track (p: Process) = + killRunProcessOnExit.Force() |> ignore + runProcess <- Some p + + let private untrack (p: Process) = + if runProcess = Some p then + runProcess <- None + + let startWithEnv envVars = fun (workingDir: string) (exePath: string) (args: string list) -> try - runningProcess |> Option.iter kill - let p = startProcess false envVars workingDir exePath args - runningProcess <- Some p + runProcess + |> Option.iter (fun p -> + kill p + untrack p + ) + + startProcess false envVars workingDir exePath args |> track with ex -> Log.always ("Cannot run: " + ex.Message) @@ -464,7 +494,9 @@ module Process = let runSyncWithEnv envVars (workingDir: string) (exePath: string) (args: string list) = try let p = startProcess false envVars workingDir exePath args + track p p.WaitForExit() + untrack p p.ExitCode with ex -> Log.always ("Cannot run: " + ex.Message) From 28427d621b4e256fdcda83dd8340ed6817e0e60c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 18:02:29 +0200 Subject: [PATCH 217/322] feat(js/ts): add `JSX.spread` for `JSX.create` props (#5141) --- src/Fable.Build/Test/JavaScript.fs | 6 +++- src/Fable.Core/Fable.Core.JS.fs | 4 +++ src/Fable.Transforms/Babel/Babel.fs | 7 +++- src/Fable.Transforms/Babel/BabelPrinter.fs | 17 ++++++---- src/Fable.Transforms/Babel/Fable2Babel.fs | 29 ++++++++++++++-- src/Fable.Transforms/Replacements.fs | 4 +++ .../Integration/CompilationTests.fs | 2 +- .../data/jsxListOptimisation/Components.fs | 19 +++++++++++ .../Components.jsx.expected | 33 +++++++++++++++++-- .../jsxListOptimisation.fsproj | 2 +- tests/React/Fable.Tests.React.fsproj | 4 ++- tests/React/__tests__/JSX_API.fs | 32 ++++++++++++++++++ 12 files changed, 142 insertions(+), 17 deletions(-) diff --git a/src/Fable.Build/Test/JavaScript.fs b/src/Fable.Build/Test/JavaScript.fs index 4b978a26ad..67f66794a9 100644 --- a/src/Fable.Build/Test/JavaScript.fs +++ b/src/Fable.Build/Test/JavaScript.fs @@ -24,6 +24,7 @@ let private testReact (isWatch: bool) = Command.WatchFableAsync( CmdLine.appendRaw "watch" >> CmdLine.appendRaw "--noCache" + >> CmdLine.appendPrefix "--exclude" "Fable.Core" // There seems to be some strange console Log writting >> CmdLine.appendRaw "--verbose" >> CmdLine.appendRaw "--runWatch" @@ -42,7 +43,10 @@ let private testReact (isWatch: bool) = |> Async.RunSynchronously |> ignore else - Command.Fable(CmdLine.appendRaw "--noCache", workingDirectory = workingDirectory) + Command.Fable( + CmdLine.appendRaw "--noCache" >> CmdLine.appendPrefix "--exclude" "Fable.Core", + workingDirectory = workingDirectory + ) Command.Run("npx", "jest", workingDirectory = workingDirectory) diff --git a/src/Fable.Core/Fable.Core.JS.fs b/src/Fable.Core/Fable.Core.JS.fs index 56a6e450aa..880df39172 100644 --- a/src/Fable.Core/Fable.Core.JS.fs +++ b/src/Fable.Core/Fable.Core.JS.fs @@ -33,6 +33,10 @@ module JSX = /// that can be resolved at compile-time. let create (componentOrTag: ElementType) (props: Prop list) : Element = nativeOnly + /// Spreads an object into the props of a JSX Element: `
`. + /// Can only be used as an element of the `props` list passed to `JSX.create`. + let spread (props: obj) : Prop = nativeOnly + /// Creates a JSX Element directly from a string template, which can be interpolated. /// When using interpolation note the holes must follow JSX syntax rules. /// E.g. holes in the middle of a string or in the position of a prop key are not valid. diff --git a/src/Fable.Transforms/Babel/Babel.fs b/src/Fable.Transforms/Babel/Babel.fs index e0428756d7..4946e3e225 100644 --- a/src/Fable.Transforms/Babel/Babel.fs +++ b/src/Fable.Transforms/Babel/Babel.fs @@ -26,7 +26,7 @@ type AssignmentOperator = /// Since the left-hand side of an assignment may be any expression in general, an expression can also be a pattern. type Expression = | CommentedExpression of comment: string * expr: Expression - | JsxElement of componentOrTag: Expression * props: (string * Expression) list * children: Expression list + | JsxElement of componentOrTag: Expression * props: JsxProp list * children: Expression list | JsxTemplate of parts: string array * values: Expression array | Literal of Literal | Identifier of Identifier @@ -535,6 +535,11 @@ type TypeParameter = | TypeParameter of name: string * bound: TypeAnnotation opt type FunctionTypeParam = | FunctionTypeParam of name: Identifier * typeAnnotation: TypeAnnotation * isOptional: bool +[] +type JsxProp = + | KeyValue of key: string * value: Expression + | Spread of value: Expression + [] module Helpers = type Expression with diff --git a/src/Fable.Transforms/Babel/BabelPrinter.fs b/src/Fable.Transforms/Babel/BabelPrinter.fs index 0dfd49ebc9..9bf515eef9 100644 --- a/src/Fable.Transforms/Babel/BabelPrinter.fs +++ b/src/Fable.Transforms/Babel/BabelPrinter.fs @@ -447,9 +447,7 @@ module PrinterExtensions = printer.Print(Array.last parts |> escape) - member printer.PrintJsxElement - (componentOrTag: Expression, props: (string * Expression) list, children: Expression list) - = + member printer.PrintJsxElement(componentOrTag: Expression, props: JsxProp list, children: Expression list) = let printTag = function | StringConstant tag -> printer.Print(tag) @@ -475,14 +473,21 @@ module PrinterExtensions = props |> List.iter ( function - | _, NullOrUndefinedOrVoid -> () - | key, StringConstant value -> printProp (fun () -> printer.Print($"%s{key}=\"%s{value}\"")) - | key, value -> + | JsxProp.KeyValue(_, NullOrUndefinedOrVoid) -> () + | JsxProp.KeyValue(key, StringConstant value) -> + printProp (fun () -> printer.Print($"%s{key}=\"%s{value}\"")) + | JsxProp.KeyValue(key, value) -> printProp (fun () -> printer.Print(key + "={") printer.Print(value) printer.Print("}") ) + | JsxProp.Spread value -> + printProp (fun () -> + printer.Print("{...") + printer.Print(value) + printer.Print("}") + ) ) printer.PopIndentation() diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 45749eb362..fabd4a3402 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -2128,6 +2128,11 @@ module Util = |> List.append thisArg |> emitExpression range macro + [] + type JsxPropExpr = + | KeyValue of key: string * value: Fable.Expr + | Spread of value: Fable.Expr + let transformJsxProps (com: IBabelCompiler) props = (Some([], []), props) ||> List.fold (fun propsAndChildren prop -> @@ -2139,7 +2144,7 @@ module Util = | Replacements.Util.ArrayOrListLiteral(children, _) -> Some(props, children) | value -> Some(props, [ value ]) else - Some((key, value) :: props, children) + Some(JsxPropExpr.KeyValue(key, value) :: props, children) // When the string value exceeds ~100 chars, Fable wraps it in a Let binding. // The Let body still contains the tuple with the prop key and a reference to the bound var; // we use the bound value (letValue) as the actual prop value. @@ -2150,7 +2155,15 @@ module Util = | Replacements.Util.ArrayOrListLiteral(children, _) -> Some(props, children) | _ -> Some(props, [ letValue ]) else - Some((key, letValue) :: props, children) + Some(JsxPropExpr.KeyValue(key, letValue) :: props, children) + | Some(props, children), + MaybeCasted(Fable.Call(_, + { + Tags = Fable.Tags.Contains "jsx-spread" + Args = [ value ] + }, + _, + _)) -> Some(JsxPropExpr.Spread value :: props, children) | Some _, e -> addError com [] e.Range "Cannot detect JSX prop key at compile time" @@ -2304,7 +2317,13 @@ but thanks to the optimisation done below we get |> List.concat let props = - props |> List.rev |> List.map (fun (k, v) -> k, transformAsExpr com ctx v) + props + |> List.rev + |> List.map ( + function + | JsxPropExpr.KeyValue(key, value) -> JsxProp.KeyValue(key, transformAsExpr com ctx value) + | JsxPropExpr.Spread value -> JsxProp.Spread(transformAsExpr com ctx value) + ) Expression.jsxElement (componentOrTag, props, children) @@ -2364,6 +2383,10 @@ but thanks to the optimisation done below we get "Expecting a static list or array literal (no generator) for JSX props" |> addErrorAndReturnNull com range |> Some + | Fable.Tags.Contains "jsx-spread", _ -> + "JSX.spread can only be used as an element of the props list passed to JSX.create" + |> addErrorAndReturnNull com range + |> Some | Fable.Tags.Contains "jsx-template", args -> match args with | StringConst template :: _ -> diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index c94e9d51b4..2af2d5e382 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1369,6 +1369,10 @@ let fableCoreLib (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp | "Fable.Core.JSX", meth -> match meth with | "create" -> Helper.LibCall(com, "JSX", "create", t, args, ?loc = r) |> withTag "jsx" |> Some + | "spread" -> + Helper.LibCall(com, "JSX", "spread", t, args, ?loc = r) + |> withTag "jsx-spread" + |> Some | "html" | "jsx" -> Helper.LibCall(com, "JSX", "html", t, args, ?loc = r) diff --git a/tests/Integration/Integration/CompilationTests.fs b/tests/Integration/Integration/CompilationTests.fs index 8d592785df..c96715bfa0 100644 --- a/tests/Integration/Integration/CompilationTests.fs +++ b/tests/Integration/Integration/CompilationTests.fs @@ -93,7 +93,7 @@ let tests = // Compile project let exitCode = Array.append - [| project; "--cwd"; $"'%s{testCaseDir}'"; "-e"; ".jsx.actual" |] + [| project; "--cwd"; $"'%s{testCaseDir}'"; "-e"; ".jsx.actual"; "--exclude"; "Fable.Core" |] precompiledLibArgs |> Fable.Cli.Entry.main diff --git a/tests/Integration/Integration/data/jsxListOptimisation/Components.fs b/tests/Integration/Integration/data/jsxListOptimisation/Components.fs index 8760bc1439..f01bd728f4 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/Components.fs +++ b/tests/Integration/Integration/data/jsxListOptimisation/Components.fs @@ -40,6 +40,8 @@ type prop = static member inline className (cls : string) : JSX_IReactProperty = "className", cls + static member inline title (value : string) : JSX_IReactProperty = "title", value + [] type Html = @@ -197,3 +199,20 @@ let divWithLongClassName = Html.div [ prop.className "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ] + +let spreadProps: obj = + createObj [ "data-testid" ==> "spread-target"; "title" ==> "from-spread" ] + +let divWithSpread = Html.div [ JSX.spread spreadProps ] + +let divWithSpreadBetweenProps = + Html.div [ prop.className "before"; JSX.spread spreadProps; prop.title "after" ] + +let divWithSpreadOverridingProps = + Html.div [ prop.title "before"; JSX.spread spreadProps ] + +let divWithTwoSpreads = + Html.div [ JSX.spread spreadProps; JSX.spread (createObj [ "title" ==> "second" ]) ] + +let divWithSpreadAndChildren = + Html.div [ JSX.spread spreadProps; prop.children [ Html.div "Child" ] ] diff --git a/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected b/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected index 8f8366f3c5..f0e4fcbb3b 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected +++ b/tests/Integration/Integration/data/jsxListOptimisation/Components.jsx.expected @@ -1,7 +1,8 @@ + import * as react from "react"; -import { empty, map, singleton, append, delay, toList } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Seq.js"; -import { int32ToString } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Util.js"; -import { rangeDouble } from "./fable_modules/fable-library-js.5.0.0-alpha.13/Range.js"; +import { empty, map, singleton, append, delay, toList } from "./fable_modules/fable-library-js.5.23.0/Seq.js"; +import { int32ToString } from "./fable_modules/fable-library-js.5.23.0/Util.js"; +import { rangeDouble } from "./fable_modules/fable-library-js.5.23.0/Range.js"; export const React = react; @@ -152,3 +153,29 @@ export function divWithElementBeforeMatchContainingForLoop(counter) { } export const divWithLongClassName =
; + +export const spreadProps = { + "data-testid": "spread-target", + title: "from-spread", +}; + +export const divWithSpread =
; + +export const divWithSpreadBetweenProps =
; + +export const divWithSpreadOverridingProps =
; + +export const divWithTwoSpreads =
; + +export const divWithSpreadAndChildren =
+
+ Child +
+
; + diff --git a/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj b/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj index 63e38f9aad..51a2e75b45 100644 --- a/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj +++ b/tests/Integration/Integration/data/jsxListOptimisation/jsxListOptimisation.fsproj @@ -11,7 +11,7 @@ - + diff --git a/tests/React/Fable.Tests.React.fsproj b/tests/React/Fable.Tests.React.fsproj index c86cba07eb..ed954b9a0c 100644 --- a/tests/React/Fable.Tests.React.fsproj +++ b/tests/React/Fable.Tests.React.fsproj @@ -28,11 +28,13 @@ - + + + diff --git a/tests/React/__tests__/JSX_API.fs b/tests/React/__tests__/JSX_API.fs index ff157d31ed..9a0b268b11 100644 --- a/tests/React/__tests__/JSX_API.fs +++ b/tests/React/__tests__/JSX_API.fs @@ -93,4 +93,36 @@ Jest.describe("JSX API tests (using React)", fun () -> Jest.test("Test that properties can be defined using `unbox`", fun () -> matchSnapshot Components.propsCanUseUnbox ) + + Jest.test("Spread props are applied to the element", fun () -> + let elem = RTL.render Components.divWithSpread + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + ) + + Jest.test("Props after a spread override the spread props", fun () -> + let elem = RTL.render Components.divWithSpreadBetweenProps + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveClass("before") + Jest.expect(target).toHaveAttribute("title", "after") + ) + + Jest.test("Spread props override the props before the spread", fun () -> + let elem = RTL.render Components.divWithSpreadOverridingProps + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + ) + + Jest.test("Several spreads are applied in order", fun () -> + let elem = RTL.render Components.divWithTwoSpreads + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "second") + ) + + Jest.test("Spread props can be combined with children", fun () -> + let elem = RTL.render Components.divWithSpreadAndChildren + let target = elem.getByTestId "spread-target" + Jest.expect(target).toHaveAttribute("title", "from-spread") + Jest.expect(target).toHaveTextContent("Child") + ) ) From 5c44e514626a230227e2aad040d439fa7da0491c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 18:49:16 +0200 Subject: [PATCH 218/322] fix(all): render warning code without repeating the tag (#5142) --- src/Fable.Cli/Main.fs | 6 ++---- tests/Integration/Compiler/CompilerMessagesTests.fs | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index d66ba6116f..3914bedb27 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -79,12 +79,10 @@ module Util = | Severity.Error -> "error" | Severity.Info -> "info" - // MSBuild-style ` `: without the code there is no way to - // discover what to put in a `// fable-disable-line` comment, and rendering it here - // also makes Fable warnings parseable by IDEs and MSBuild loggers. + // MSBuild-style ` `: the code carries the FABLE prefix, so it replaces the tag let tag = match log.Code with - | Some code -> $"%s{log.Tag} %s{code}" + | Some code -> code | None -> log.Tag match log.Range with diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index b55fd2a616..b09a16aedd 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -348,5 +348,7 @@ open System.Globalization match formatted with | [] -> failwith "Expected a FABLE0100 warning" - | messages -> equal true (messages |> List.forall (fun m -> m.Contains "warning FABLE FABLE0100:")) + | messages -> + equal true (messages |> List.forall (fun m -> m.Contains "warning FABLE0100:")) + equal false (messages |> List.exists (fun m -> m.Contains "FABLE FABLE")) ] From 3b8a9e218d27a47074b894f4768466f74c5a0f8e Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 19:57:00 +0200 Subject: [PATCH 219/322] feat(js/ts): derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) --- src/Fable.Core/Fable.Core.Types.fs | 5 +- src/Fable.Transforms/Babel/Fable2Babel.fs | 1 + src/Fable.Transforms/FSharp2Fable.Util.fs | 217 +++++++++++++++++ src/Fable.Transforms/FSharp2Fable.fs | 67 ++++-- .../Compiler/CompilerMessagesTests.fs | 220 +++++++++++++++++- tests/Js/Main/JsInteropTests.fs | 207 +++++++++++++++- 6 files changed, 691 insertions(+), 26 deletions(-) diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index 0ef2cd349e..ac0409de56 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -87,9 +87,12 @@ type TypeScriptTaggedUnionAttribute(tagName: string, caseRules: CaseRules) = /// /// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches. -/// Use it when several cases share a tag value. +/// Use it when several cases share a tag value and their fields cannot tell them apart. /// /// +/// The test must be true for every value of its case. Its negation is used to test +/// the other cases sharing the tag, so one case per group may be left without a test. +/// /// The test is a JS expression in Emit syntax, where $0 is the tested value: /// /// [<TypeScriptTaggedUnion("state")>] diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index fabd4a3402..87cc941525 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -474,6 +474,7 @@ module Reflection = ] |> libReflectionCall com ctx r "class" + // Keep in sync with FSharp2Fable.Util.tryRuntimeTypeTestKey, which predicts these tests let transformTypeTest (com: IBabelCompiler) ctx range expr typ : Expression = let warnAndEvalToFalse msg = WarningCodes.typeTestEvalsToFalse msg |> addWarningWithCode com [] range diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 33613729e6..e37b3ec3e6 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -3303,3 +3303,220 @@ module Util = | Imported com ctx r typ None imported -> imported | Try (tryGetIdentFromScope ctx r (Some typ)) expr, _ -> expr | _ -> getValueMemberRef v |> memberIdent com ctx r typ v + + /// Runtime checks that tell apart TypeScriptTaggedUnion cases sharing a tag value + [] + type TaggedUnionCaseTest = + | HasField of name: string + | LacksField of name: string + | FieldOfType of name: string * typ: Fable.Type + | NotCaseTest of macro: string + + let private tryTaggedUnionTagKey (rule: CaseRules) (unionCase: FSharpUnionCase) = + match transformStringEnum rule unionCase with + | Fable.Value(Fable.StringConstant s, _) -> Some("s:" + s) + | Fable.Value(Fable.BoolConstant b, _) -> Some("b:" + string b) + | Fable.Value(Fable.NumberConstant(v, _), _) -> Some("n:" + string v) + | _ -> None + + let taggedUnionTagDisplay (rule: CaseRules) (unionCase: FSharpUnionCase) = + match FsUnionCase.CompiledValue unionCase with + | Some(CompiledValue.Integer i) -> string i + | Some(CompiledValue.Float f) -> string f + | Some(CompiledValue.Boolean b) -> (string b).ToLowerInvariant() + | None -> + match transformStringEnum rule unionCase with + | Fable.Value(Fable.StringConstant s, _) -> $"\"%s{s}\"" + | _ -> "the same tag" + + type private TaggedUnionField = + { + Name: string + Type: Fable.Type + Display: string + } + + /// Keep in sync with Fable2Babel.Reflection.transformTypeTest: equal keys are the same runtime test, None is untestable + let rec private tryRuntimeTypeTestKey (com: Compiler) (typ: Fable.Type) : string option = + match typ with + | Fable.String + | Fable.Char -> Some "string" + | Fable.Boolean -> Some "boolean" + | Fable.Number(Decimal, _) -> Some "decimal" + | Fable.Number((Int64 | UInt64 | Int128 | UInt128 | BigInt | NativeInt | UNativeInt), _) -> Some "bigint" + | Fable.Number _ -> Some "number" + | Fable.Regex -> Some "regexp" + | Fable.LambdaType _ + | Fable.DelegateType _ -> Some "function" + | Fable.Array _ + | Fable.Tuple _ -> Some "array" + | Fable.List _ -> Some "list" + | Fable.Nullable(genArg, _) -> tryRuntimeTypeTestKey com genArg + | Fable.DeclaredType(entRef, _) -> + match com.TryGetEntity entRef with + | None -> None + | Some _ when isReplacementCandidate entRef -> None + | Some ent when hasAttribute Atts.stringEnum ent -> Some "string" + | Some ent when hasAttribute Atts.erase ent && ent.IsFSharpUnion -> + match ent.UnionCases with + | [ uci ] when List.isSingle uci.UnionCaseFields -> + tryRuntimeTypeTestKey com uci.UnionCaseFields[0].FieldType + | cases when cases |> List.forall (fun c -> List.isEmpty c.UnionCaseFields) -> Some "string" + | _ -> None + | Some ent when + ent.IsMeasure + || isErasedOrStringEnumEntity ent + || isPojoDefinedByConsArgsEntity ent + -> + None + | Some ent when ent.IsInterface && not (isGlobalOrImportedEntity ent) -> None + | Some ent -> Some("instanceof:" + ent.FullName) + | _ -> None + + let rec private isSubtypeOf (com: Compiler) (baseFullName: string) (entRef: Fable.EntityRef) = + entRef.FullName = baseFullName + || match com.TryGetEntity entRef with + | None -> false + | Some ent -> + ent.AllInterfaces |> Seq.exists (fun ifc -> ifc.Entity.FullName = baseFullName) + || ( + match ent.BaseType with + | Some baseType -> isSubtypeOf com baseFullName baseType.Entity + | None -> false + ) + + /// Whether every value of `sub` also passes the runtime test of `super` + let private passesTestOf (com: Compiler) (super: Fable.Type) (sub: Fable.Type) = + match super, sub with + | Fable.DeclaredType(superRef, _), Fable.DeclaredType(subRef, _) -> isSubtypeOf com superRef.FullName subRef + | _ -> false + + let private tryDistinguishTaggedUnionCase + (com: Compiler) + (mine: TaggedUnionField list) + (other: TaggedUnionField list) + = + let isRequired (f: TaggedUnionField) = + match f.Type with + | Fable.Option _ -> false + | _ -> true + + let hasName name (fields: TaggedUnionField list) = + fields |> List.exists (fun f -> f.Name = name) + + let byPresence = + mine + |> List.tryFind (fun f -> isRequired f && not (hasName f.Name other)) + |> Option.map (fun f -> TaggedUnionCaseTest.HasField f.Name) + + let byAbsence () = + other + |> List.tryFind (fun f -> isRequired f && not (hasName f.Name mine)) + |> Option.map (fun f -> TaggedUnionCaseTest.LacksField f.Name) + + let byType () = + mine + |> List.tryPick (fun f -> + match List.tryFind (fun (o: TaggedUnionField) -> o.Name = f.Name) other with + | None -> None + | Some o -> + match tryRuntimeTypeTestKey com f.Type, tryRuntimeTypeTestKey com o.Type with + | Some myKey, Some otherKey when + myKey <> otherKey + && not (passesTestOf com f.Type o.Type) + && not (passesTestOf com o.Type f.Type) + -> + Some(TaggedUnionCaseTest.FieldOfType(f.Name, f.Type)) + | _ -> None + ) + + byPresence |> Option.orElseWith byAbsence |> Option.orElseWith byType + + let private describeUntestableType (com: Compiler) (f: TaggedUnionField) = + let kind = + match f.Type with + | Fable.Any -> "which can hold any value" + | Fable.GenericParam _ -> "a generic parameter unknown at runtime" + | Fable.DeclaredType(entRef, _) -> + match com.TryGetEntity entRef with + | Some ent when isPojoDefinedByConsArgsEntity ent -> "a Pojo that is a plain object at runtime" + | Some ent when ent.IsInterface -> "an interface that does not exist at runtime" + | Some ent when isErasedOrStringEnumEntity ent -> "an erased type" + | _ -> "a type that cannot be tested at runtime" + | _ -> "a type that cannot be tested at runtime" + + $"field '%s{f.Name}' has type '%s{f.Display}', %s{kind}" + + /// Why no field of `mine` tells it apart from `other` at runtime + let private explainIndistinguishable (com: Compiler) (mine: TaggedUnionField list) (other: TaggedUnionField list) = + let shared = + mine + |> List.choose (fun f -> other |> List.tryFind (fun o -> o.Name = f.Name) |> Option.map (fun o -> f, o)) + + match shared |> List.tryFind (fun (f, o) -> f.Type <> o.Type) with + | None when List.length shared = List.length mine && List.length shared = List.length other -> + "have the same fields" + | None -> "differ only by optional fields" + | Some(f, o) -> + let reason = + match tryRuntimeTypeTestKey com f.Type, tryRuntimeTypeTestKey com o.Type with + | None, _ -> describeUntestableType com f + | _, None -> describeUntestableType com o + | Some myKey, Some otherKey when myKey = otherKey -> + $"field '%s{f.Name}' has types '%s{f.Display}' and '%s{o.Display}', which look the same at runtime" + | _ -> + let sub, super = + if passesTestOf com f.Type o.Type then + o, f + else + f, o + + $"field '%s{f.Name}' has types '%s{f.Display}' and '%s{o.Display}', and '%s{sub.Display}' is a subtype of '%s{super.Display}'" + + "cannot be told apart at runtime: " + reason + + /// One check per other case with the same tag value, or the case the fields cannot exclude + let deriveTaggedUnionCaseTests + (com: Compiler) + (tdef: FSharpEntity) + (rule: CaseRules) + (unionCase: FSharpUnionCase) + : Result + = + let fieldsOf (uc: FSharpUnionCase) = + uc.Fields + |> Seq.map (fun fi -> + { + Name = FsField.FSharpFieldName fi + Type = makeType Map.empty fi.FieldType + Display = fi.FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + } + ) + |> List.ofSeq + + match tryTaggedUnionTagKey rule unionCase with + | None -> Ok [] + | Some myTag -> + let myFields = fieldsOf unionCase + + tdef.UnionCases + |> Seq.filter (fun uc -> uc.Name <> unionCase.Name && tryTaggedUnionTagKey rule uc = Some myTag) + |> Seq.fold + (fun acc other -> + match acc with + | Error _ -> acc + | Ok tests -> + let otherFields = fieldsOf other + + let byCaseTest () = + FsUnionCase.CaseTest other |> Option.map TaggedUnionCaseTest.NotCaseTest + + match + tryDistinguishTaggedUnionCase com myFields otherFields + |> Option.orElseWith byCaseTest + with + | Some test -> Ok(tests @ [ test ]) + | None -> Error(other, explainIndistinguishable com myFields otherFields) + ) + (Ok []) + |> Result.map List.distinct diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 7307c72835..a22a9766bb 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -578,14 +578,6 @@ let private transformUnionCaseTest let! unionExpr = transformExpr com ctx [] unionExpr let unionPattern = getUnionPattern fsType unionCase - do - match unionPattern, FsUnionCase.CaseTest unionCase with - | TypeScriptTaggedUnion _, _ - | _, None -> () - | _, Some _ -> - WarningCodes.caseTestIgnored unionCase.Name - |> addWarningWithCode com ctx.InlinePath r - match unionPattern with | ErasedUnionCase -> return "Cannot test erased union cases" |> addErrorAndReturnNull com ctx.InlinePath r @@ -622,7 +614,7 @@ let private transformUnionCaseTest + (getFsTypeFullName fsType) |> addErrorAndReturnNull com ctx.InlinePath r - | TypeScriptTaggedUnion(_, _, tagName, rule) -> + | TypeScriptTaggedUnion(tdef, _, tagName, rule) -> let isCompiledValue, value = match FsUnionCase.CompiledValue unionCase with | None -> false, transformStringEnum rule unionCase @@ -640,19 +632,43 @@ let private transformUnionCaseTest let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) makeEqOp r getTag value BinaryEqual - match FsUnionCase.CaseTest unionCase with - | Some macro -> - // The union value is tested twice, so bind it first if it is not a plain value - let makeTest unionExpr = - let caseTest = emitExpr r Fable.Boolean [ unionExpr ] macro - makeLogOp r (makeTagTest unionExpr) caseTest LogicalAnd + let makeDerivedTest unionExpr test = + match test with + | TaggedUnionCaseTest.HasField name -> + emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "$0 in $1" + | TaggedUnionCaseTest.LacksField name -> + emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "!($0 in $1)" + | TaggedUnionCaseTest.FieldOfType(name, typ) -> + let field = + Fable.Get(unionExpr, Fable.FieldInfo.Create(name, fieldType = typ), typ, r) + + Fable.Test(field, Fable.TypeTest typ, r) + | TaggedUnionCaseTest.NotCaseTest macro -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})" + + let caseTests: (Fable.Expr -> Fable.Expr) list = + match FsUnionCase.CaseTest unionCase with + | Some macro -> [ fun unionExpr -> emitExpr r Fable.Boolean [ unionExpr ] macro ] + | None -> + match deriveTaggedUnionCaseTests com tdef rule unionCase with + | Ok tests -> tests |> List.map (fun test unionExpr -> makeDerivedTest unionExpr test) + | Error(other, reason) -> + let tag = taggedUnionTagDisplay rule unionCase - if canHaveSideEffects com unionExpr then - let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type - return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) - else - return makeTest unionExpr - | None -> return makeTagTest unionExpr + $"Cases '%s{unionCase.Name}' and '%s{other.Name}' of '%s{tdef.DisplayName}' share the tag value %s{tag} and %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}' or '%s{other.Name}'." + |> addError com ctx.InlinePath r + + [] + + let makeTest unionExpr = + (makeTagTest unionExpr, caseTests) + ||> List.fold (fun acc test -> makeLogOp r acc (test unionExpr) LogicalAnd) + + // The union value is read once per test, so bind it first if it is not a plain value + if canHaveSideEffects com unionExpr && not (List.isEmpty caseTests) then + let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type + return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) + else + return makeTest unionExpr | OptionUnion _ -> let kind = @@ -2220,11 +2236,20 @@ let rec private getUsedRootNames (com: Compiler) (usedNames: Set) decls | FSharpImplementationFileDeclaration.InitAction _ -> usedNames ) +let private warnIgnoredCaseTests (com: FableCompiler) ctx (ent: FSharpEntity) = + if ent.IsFSharpUnion && not (hasAttrib Atts.tsTaggedUnion ent.Attributes) then + for uci in ent.UnionCases do + if (FsUnionCase.CaseTest uci).IsSome then + WarningCodes.caseTestIgnored uci.Name + |> addWarningWithCode (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = fsDecls |> List.collect (fun fsDecl -> match fsDecl with | FSharpImplementationFileDeclaration.Entity(fsEnt, sub) -> + warnIgnoredCaseTests com ctx fsEnt + match sub with | [] when isIgnoredLeafEntity fsEnt -> [] | [] -> diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index b09a16aedd..f570155bee 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -296,11 +296,10 @@ open Fable.Core type Id = | [] Name of string | [] Slug of string - -let isName (id: Id) = match id with Name _ -> true | Slug _ -> false """ compile source - |> Assert.Exists.warningWith "is ignored: it is only supported on TypeScriptTaggedUnion cases" + |> Assert.Exists.warningWith "CaseTest attribute on 'Name' is ignored: it is only supported on TypeScriptTaggedUnion cases" + |> Assert.Exists.warningWith "CaseTest attribute on 'Slug' is ignored: it is only supported on TypeScriptTaggedUnion cases" |> ignore testCase "CaseTest in a TypeScriptTaggedUnion does not emit warning" <| fun _ -> @@ -351,4 +350,219 @@ open System.Globalization | messages -> equal true (messages |> List.forall (fun m -> m.Contains "warning FABLE0100:")) equal false (messages |> List.exists (fun m -> m.Contains "FABLE FABLE")) + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when their fields tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +type Payload(x: int) = + member _.X = x + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string + | [] LogString of message: string + | [] LogPayload of message: Payload + +let describe (t: Terminal) = + match t with + | DoneError _ -> "error" + | LogString _ -> "string" + | LogPayload _ -> "payload" + | Done _ -> "done" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when their fields cannot tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Result = + | [] Ok of ok: bool + | [] Failed of ok: bool + +let isFailed (r: Result) = match r with Failed _ -> true | Ok _ -> false +""" + compile source + |> Assert.Exists.errorWith "of 'Result' share the tag value \"done\" and have the same fields. Add a CaseTest attribute to" + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when the other case has a CaseTest" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Result = + | [] Ok of ok: bool + | [] Failed of ok: bool + +let isOk (r: Result) = r.IsOk +let isFailed (r: Result) = match r with Failed _ -> true | Ok _ -> false +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when more than one case has no CaseTest" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Job = + | [] Build of kind: string + | [] Test of kind: string + | [] Other of kind: string + +let isOther (j: Job) = j.IsOther +""" + compile source + |> Assert.Exists.errorWith "Cases 'Other' and 'Test' of 'Job' share the tag value \"run\" and have the same fields. Add a CaseTest attribute to 'Other' or 'Test'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when one field type includes the other" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Log = + | [] LogString of message: string + | [] LogObject of message: obj + +let isObject (l: Log) = l.IsLogObject +let isString (l: Log) = l.IsLogString +""" + compile source + |> Assert.Exists.errorWith "Cases 'LogObject' and 'LogString' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'obj', which can hold any value. Add a CaseTest attribute to 'LogObject' or 'LogString'." + |> Assert.Exists.errorWith "Cases 'LogString' and 'LogObject' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'obj', which can hold any value. Add a CaseTest attribute to 'LogString' or 'LogObject'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when one field type is a base class of the other" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = class end +type Dog() = inherit Animal() + +[] +type Pet = + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + +let isAnimal (p: Pet) = p.IsPetAnimal +let isDog (p: Pet) = p.IsPetDog +""" + compile source + |> Assert.Exists.errorWith "Cases 'PetAnimal' and 'PetDog' of 'Pet' share the tag value \"pet\" and cannot be told apart at runtime: field 'pet' has types 'Animal' and 'Dog', and 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'PetAnimal' or 'PetDog'." + |> Assert.Exists.errorWith "Cases 'PetDog' and 'PetAnimal' of 'Pet' share the tag value \"pet\" and cannot be told apart at runtime: field 'pet' has types 'Dog' and 'Animal', and 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'PetDog' or 'PetAnimal'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag compile when a CaseTest narrows a base class field" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = class end + +[] +type Dog() = + inherit Animal() + member _.Bark() = "woof" + +[] +type Pet = + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + +let isAnimal (p: Pet) = p.IsPetAnimal +let isDog (p: Pet) = p.IsPetDog +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when only a Pojo field type tells them apart" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Options(name: string) = + member val name = name + +[] +type Log = + | [] LogString of message: string + | [] LogOptions of message: Options + +let isOptions (l: Log) = l.IsLogOptions +let isString (l: Log) = l.IsLogString +""" + compile source + |> Assert.Exists.errorWith "Cases 'LogOptions' and 'LogString' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'Options', a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'LogOptions' or 'LogString'." + |> Assert.Exists.errorWith "Cases 'LogString' and 'LogOptions' of 'Log' share the tag value \"log\" and cannot be told apart at runtime: field 'message' has type 'Options', a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'LogString' or 'LogOptions'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag emit error when only interfaces tell them apart" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + +[] +type Shape = + | [] Circle of shape: Circle + | [] Rect of shape: Rect + +let isRect (s: Shape) = s.IsRect +""" + compile source + |> Assert.Exists.errorWith "Cases 'Rect' and 'Circle' of 'Shape' share the tag value \"shape\" and cannot be told apart at runtime: field 'shape' has type 'Rect', an interface that does not exist at runtime. Add a CaseTest attribute to 'Rect' or 'Circle'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a number tag emit error showing the tag value" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Status = + | [] Ok of value: string + | [] Failed of value: string + +let isOk (s: Status) = s.IsOk +""" + compile source + |> Assert.Exists.errorWith "Cases 'Ok' and 'Failed' of 'Status' share the tag value 1 and have the same fields. Add a CaseTest attribute to 'Ok' or 'Failed'." + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a boolean tag emit error showing the tag value" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Status = + | [] Ok of value: string + | [] Warn of value: string + +let isOk (s: Status) = s.IsOk +""" + compile source + |> Assert.Exists.errorWith "Cases 'Ok' and 'Warn' of 'Status' share the tag value true and have the same fields. Add a CaseTest attribute to 'Ok' or 'Warn'." + |> ignore ] diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index d8a914b13f..051faf7637 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -398,6 +398,59 @@ module TaggedUnion = | [] DoneFailed of ok: bool | Pending + type Payload(x: int) = + member _.X = x + + type Animal() = + member _.Kind = "animal" + + [] + type Dog() = + inherit Animal() + member _.Kind = "dog" + member _.Bark() = "woof" + + [] + type DerivedTag = + | [] Done of value: float + | [] DoneError of error: string + | [] DoneWithExtra of value: float * extra: string + | [] LogString of message: string + | [] LogNumber of message: float + | [] LogPayload of message: Payload + | [] PetAnimal of pet: Animal + | [] PetDog of pet: Dog + | Pending + + [] + type Options(name: string) = + member val name = name + + [] + type PojoField = + | [] LogString of message: string + | [] LogOptions of message: Options + + [] + type Outcome = + | [] Ok of ok: bool + | [] Failed of ok: bool + + [] + type Job = + | [] Build of kind: string + | [] Test of kind: string + | [] Other of kind: string + | [] Idle + + [] + type NumberTag = + | [] Text of text: string + | [] Count of count: int + | [] On of flag: bool + | [] Off of flag: bool + | [] Idle + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -1098,6 +1151,8 @@ let tests = equal (TaggedUnion.FooGeneric.Bar 10) fooGeneric +#endif + testCase "Case testing with TS tagged unions whose cases share a tag works" <| fun () -> let describe = function | TaggedUnion.SharedTag.Done v -> sprintf "done: %.1f" v @@ -1108,7 +1163,6 @@ let tests = TaggedUnion.SharedTag.DoneError "oops" |> describe |> equal "error: oops" TaggedUnion.SharedTag.DoneFailed false |> describe |> equal "failed" TaggedUnion.SharedTag.Pending |> describe |> equal "pending" - !!{| state = "done"; error = "raw" |} |> describe |> equal "error: raw" (TaggedUnion.SharedTag.DoneError "oops").IsDoneError |> equal true (TaggedUnion.SharedTag.Done 1.5).IsDoneError |> equal false @@ -1119,6 +1173,157 @@ let tests = TaggedUnion.SharedTag.DoneError "oops" (next ()).IsDoneError |> equal true calls |> equal 1 + + testCase "Case testing with TS tagged unions whose cases share a tag is derived from the fields" <| fun () -> + let describe = function + | TaggedUnion.DerivedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.DerivedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.DerivedTag.DoneWithExtra (v, e) -> sprintf "extra: %.1f %s" v e + | TaggedUnion.DerivedTag.LogString m -> sprintf "string: %s" m + | TaggedUnion.DerivedTag.LogNumber n -> sprintf "number: %.1f" n + | TaggedUnion.DerivedTag.LogPayload p -> sprintf "payload: %i" p.X + | TaggedUnion.DerivedTag.PetAnimal a -> "animal: " + a.Kind + | TaggedUnion.DerivedTag.PetDog d -> "dog: " + d.Kind + | TaggedUnion.DerivedTag.Pending -> "pending" + TaggedUnion.DerivedTag.Done 1.5 |> describe |> equal "done: 1.5" + TaggedUnion.DerivedTag.DoneError "oops" |> describe |> equal "error: oops" + TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e") |> describe |> equal "extra: 2.5 e" + TaggedUnion.DerivedTag.LogString "hi" |> describe |> equal "string: hi" + TaggedUnion.DerivedTag.LogNumber 2.5 |> describe |> equal "number: 2.5" + TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3) |> describe |> equal "payload: 3" + TaggedUnion.DerivedTag.Pending |> describe |> equal "pending" + (TaggedUnion.DerivedTag.Done 1.5).IsDone |> equal true + (TaggedUnion.DerivedTag.Done 1.5).IsDoneError |> equal false + (TaggedUnion.DerivedTag.Done 1.5).IsDoneWithExtra |> equal false + (TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e")).IsDone |> equal false + (TaggedUnion.DerivedTag.DoneWithExtra (2.5, "e")).IsDoneWithExtra |> equal true + (TaggedUnion.DerivedTag.LogString "hi").IsLogString |> equal true + (TaggedUnion.DerivedTag.LogString "hi").IsLogNumber |> equal false + (TaggedUnion.DerivedTag.LogString "hi").IsLogPayload |> equal false + (TaggedUnion.DerivedTag.LogNumber 2.5).IsLogNumber |> equal true + (TaggedUnion.DerivedTag.LogNumber 2.5).IsLogString |> equal false + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogPayload |> equal true + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogString |> equal false + (TaggedUnion.DerivedTag.LogPayload (TaggedUnion.Payload 3)).IsLogNumber |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses a CaseTest to narrow a base class field" <| fun () -> + let describe = function + | TaggedUnion.DerivedTag.PetAnimal a -> "animal: " + a.Kind + | TaggedUnion.DerivedTag.PetDog d -> "dog: " + d.Kind + | _ -> "other" + TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal()) |> describe |> equal "animal: animal" + TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog()) |> describe |> equal "dog: dog" + (TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal())).IsPetAnimal |> equal true + (TaggedUnion.DerivedTag.PetAnimal (TaggedUnion.Animal())).IsPetDog |> equal false + (TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog())).IsPetDog |> equal true + (TaggedUnion.DerivedTag.PetDog (TaggedUnion.Dog())).IsPetAnimal |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses a CaseTest for a Pojo field" <| fun () -> + let describe = function + | TaggedUnion.PojoField.LogOptions o -> "options: " + o.name + | TaggedUnion.PojoField.LogString s -> "string: " + s + TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x") |> describe |> equal "options: x" + TaggedUnion.PojoField.LogString "hi" |> describe |> equal "string: hi" + (TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x")).IsLogOptions |> equal true + (TaggedUnion.PojoField.LogOptions (TaggedUnion.Options "x")).IsLogString |> equal false + (TaggedUnion.PojoField.LogString "hi").IsLogString |> equal true + (TaggedUnion.PojoField.LogString "hi").IsLogOptions |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses the CaseTest of the other case" <| fun () -> + let describe = function + | TaggedUnion.Outcome.Failed _ -> "failed" + | TaggedUnion.Outcome.Ok _ -> "ok" + TaggedUnion.Outcome.Ok true |> describe |> equal "ok" + TaggedUnion.Outcome.Failed false |> describe |> equal "failed" + (TaggedUnion.Outcome.Ok true).IsOk |> equal true + (TaggedUnion.Outcome.Ok true).IsFailed |> equal false + (TaggedUnion.Outcome.Failed false).IsFailed |> equal true + (TaggedUnion.Outcome.Failed false).IsOk |> equal false + + testCase "Case testing with TS tagged unions whose cases share a tag uses the CaseTest of the other cases" <| fun () -> + let describe = function + | TaggedUnion.Job.Other k -> "other: " + k + | TaggedUnion.Job.Build k -> "build: " + k + | TaggedUnion.Job.Test k -> "test: " + k + | TaggedUnion.Job.Idle -> "idle" + TaggedUnion.Job.Build "build" |> describe |> equal "build: build" + TaggedUnion.Job.Test "test" |> describe |> equal "test: test" + TaggedUnion.Job.Other "lint" |> describe |> equal "other: lint" + TaggedUnion.Job.Idle |> describe |> equal "idle" + (TaggedUnion.Job.Other "lint").IsOther |> equal true + (TaggedUnion.Job.Other "lint").IsBuild |> equal false + (TaggedUnion.Job.Other "lint").IsTest |> equal false + (TaggedUnion.Job.Build "build").IsBuild |> equal true + (TaggedUnion.Job.Build "build").IsOther |> equal false + (TaggedUnion.Job.Test "test").IsTest |> equal true + (TaggedUnion.Job.Test "test").IsOther |> equal false + + testCase "Case testing with TS tagged unions whose cases share a number tag works" <| fun () -> + let describe = function + | TaggedUnion.NumberTag.Text t -> "text: " + t + | TaggedUnion.NumberTag.Count c -> sprintf "count: %i" c + | TaggedUnion.NumberTag.On _ -> "on" + | TaggedUnion.NumberTag.Off _ -> "off" + | TaggedUnion.NumberTag.Idle -> "idle" + TaggedUnion.NumberTag.Text "hi" |> describe |> equal "text: hi" + TaggedUnion.NumberTag.Count 3 |> describe |> equal "count: 3" + TaggedUnion.NumberTag.On true |> describe |> equal "on" + TaggedUnion.NumberTag.Off false |> describe |> equal "off" + TaggedUnion.NumberTag.Idle |> describe |> equal "idle" + (TaggedUnion.NumberTag.Text "hi").IsText |> equal true + (TaggedUnion.NumberTag.Text "hi").IsCount |> equal false + (TaggedUnion.NumberTag.Count 3).IsCount |> equal true + (TaggedUnion.NumberTag.Count 3).IsText |> equal false + (TaggedUnion.NumberTag.On true).IsOn |> equal true + (TaggedUnion.NumberTag.On true).IsOff |> equal false + (TaggedUnion.NumberTag.Off false).IsOff |> equal true + (TaggedUnion.NumberTag.Off false).IsOn |> equal false + +#if FABLE_COMPILER + testCase "Case testing with TS tagged unions whose cases share a tag works on raw JS objects" <| fun () -> + let describeShared = function + | TaggedUnion.SharedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.SharedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.SharedTag.DoneFailed _ -> "failed" + | TaggedUnion.SharedTag.Pending -> "pending" + let describeDerived = function + | TaggedUnion.DerivedTag.Done v -> sprintf "done: %.1f" v + | TaggedUnion.DerivedTag.DoneError e -> sprintf "error: %s" e + | TaggedUnion.DerivedTag.LogString m -> sprintf "string: %s" m + | TaggedUnion.DerivedTag.LogNumber n -> sprintf "number: %.1f" n + | _ -> "other" + let describePojo = function + | TaggedUnion.PojoField.LogOptions o -> "options: " + o.name + | TaggedUnion.PojoField.LogString s -> "string: " + s + let describeOutcome = function + | TaggedUnion.Outcome.Failed _ -> "failed" + | TaggedUnion.Outcome.Ok _ -> "ok" + let describeJob = function + | TaggedUnion.Job.Other k -> "other: " + k + | TaggedUnion.Job.Build k -> "build: " + k + | TaggedUnion.Job.Test k -> "test: " + k + | TaggedUnion.Job.Idle -> "idle" + let describeNumber = function + | TaggedUnion.NumberTag.Text t -> "text: " + t + | TaggedUnion.NumberTag.Count c -> sprintf "count: %i" c + | TaggedUnion.NumberTag.On _ -> "on" + | TaggedUnion.NumberTag.Off _ -> "off" + | TaggedUnion.NumberTag.Idle -> "idle" + !!{| state = "done"; error = "raw" |} |> describeShared |> equal "error: raw" + !!{| state = "done"; error = "raw" |} |> describeDerived |> equal "error: raw" + !!{| state = "log"; message = 1 |} |> describeDerived |> equal "number: 1.0" + !!{| state = "log"; message = "raw" |} |> describeDerived |> equal "string: raw" + !!{| state = "log"; message = {| name = "raw" |} |} |> describePojo |> equal "options: raw" + !!{| state = "log"; message = "raw" |} |> describePojo |> equal "string: raw" + !!{| state = "done"; ok = true |} |> describeOutcome |> equal "ok" + !!{| state = "done"; ok = false |} |> describeOutcome |> equal "failed" + !!{| state = "run"; kind = "deploy" |} |> describeJob |> equal "other: deploy" + !!{| state = "run"; kind = "test" |} |> describeJob |> equal "test: test" + !!{| code = 1; count = 2 |} |> describeNumber |> equal "count: 2" + !!{| code = 1; text = "raw" |} |> describeNumber |> equal "text: raw" + !!{| code = 2; flag = true |} |> describeNumber |> equal "on" + !!{| code = 2; flag = false |} |> describeNumber |> equal "off" + !!{| code = 3 |} |> describeNumber |> equal "idle" #endif testCase "Pattern matching with StringEnum works" <| fun () -> From 8cc210773e7b30bcf1071388d603b5c115cccd85 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 19:57:17 +0200 Subject: [PATCH 220/322] feat(js/ts): support CaseTest on Erase union cases (#5131) --- src/Fable.Core/Fable.Core.Types.fs | 18 ++- src/Fable.Transforms/FSharp2Fable.Util.fs | 80 ++++++++-- src/Fable.Transforms/FSharp2Fable.fs | 106 ++++++++++--- src/Fable.Transforms/Global/WarningCodes.fs | 8 +- .../Compiler/BehaviorWarningTests.fs | 1 - .../Compiler/CompilerMessagesTests.fs | 147 +++++++++++++++++- tests/Js/Main/JsInteropTests.fs | 54 +++++++ 7 files changed, 370 insertions(+), 44 deletions(-) diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index ac0409de56..1d956680e1 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -86,20 +86,30 @@ type TypeScriptTaggedUnionAttribute(tagName: string, caseRules: CaseRules) = new(tagName: string) = TypeScriptTaggedUnionAttribute(tagName, CaseRules.LowerFirst) /// -/// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches. -/// Use it when several cases share a tag value and their fields cannot tell them apart. +/// Adds a test to a case of a TypeScriptTaggedUnion, checked after the tag matches, +/// or to a case of an Erase union, checked after its type test. When the type of +/// an erased case cannot be tested at runtime, the test is used on its own. +/// Use it when several cases share a tag value and their fields cannot tell them apart, +/// or when erased cases share a runtime type. /// /// /// The test must be true for every value of its case. Its negation is used to test -/// the other cases sharing the tag, so one case per group may be left without a test. +/// the other cases sharing the tag, or the erased cases with the same runtime type, +/// so one case per group may be left without a test. /// -/// The test is a JS expression in Emit syntax, where $0 is the tested value: +/// The test is a JS expression in Emit syntax, where $0 is the tested value. +/// It only applies when targeting JavaScript or TypeScript. /// /// [<TypeScriptTaggedUnion("state")>] /// type Terminal = /// | [<CompiledName("done"); CaseTest("'value' in $0")>] Done of value: float /// | [<CompiledName("done"); CaseTest("'error' in $0")>] DoneError of error: string /// | Pending +/// +/// [<Erase>] +/// type Shape = +/// | [<CaseTest("'radius' in $0")>] Circle of Circle +/// | [<CaseTest("'width' in $0")>] Rect of Rect /// /// type CaseTestAttribute(test: string) = diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index e37b3ec3e6..449a74bb6f 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -3432,18 +3432,25 @@ module Util = byPresence |> Option.orElseWith byAbsence |> Option.orElseWith byType + /// Why a type is known to have no runtime test, None when it has one or when this is unknown + let private tryUntestableKind (com: Compiler) (typ: Fable.Type) = + match typ with + | Fable.Any -> Some "which can hold any value" + | Fable.GenericParam _ -> Some "a generic parameter unknown at runtime" + | Fable.DeclaredType(entRef, _) when (tryRuntimeTypeTestKey com typ).IsNone -> + match com.TryGetEntity entRef with + | Some ent when isPojoDefinedByConsArgsEntity ent -> Some "a Pojo that is a plain object at runtime" + | Some ent when ent.IsInterface -> Some "an interface that does not exist at runtime" + | Some ent when isErasedOrStringEnumEntity ent -> Some "an erased type" + | _ -> None + | _ -> None + + let isUntestableType (com: Compiler) (typ: Fable.Type) = (tryUntestableKind com typ).IsSome + let private describeUntestableType (com: Compiler) (f: TaggedUnionField) = let kind = - match f.Type with - | Fable.Any -> "which can hold any value" - | Fable.GenericParam _ -> "a generic parameter unknown at runtime" - | Fable.DeclaredType(entRef, _) -> - match com.TryGetEntity entRef with - | Some ent when isPojoDefinedByConsArgsEntity ent -> "a Pojo that is a plain object at runtime" - | Some ent when ent.IsInterface -> "an interface that does not exist at runtime" - | Some ent when isErasedOrStringEnumEntity ent -> "an erased type" - | _ -> "a type that cannot be tested at runtime" - | _ -> "a type that cannot be tested at runtime" + tryUntestableKind com f.Type + |> Option.defaultValue "a type that cannot be tested at runtime" $"field '%s{f.Name}' has type '%s{f.Display}', %s{kind}" @@ -3520,3 +3527,56 @@ module Util = ) (Ok []) |> Result.map List.distinct + + /// Checks added to the type test of an erased union case, or why the type test is not enough + [] + type ErasedUnionCaseTests = + | NotCaseTests of macros: string list + | Untestable of reason: string + | Ambiguous of other: FSharpUnionCase * reason: string + + /// Negated CaseTests of the other cases whose values pass this case's type test + let deriveErasedUnionCaseTests + (com: Compiler) + (tdef: FSharpEntity) + (unionCase: FSharpUnionCase) + (payloadType: FSharpUnionCase -> Fable.Type option) + : ErasedUnionCaseTests + = + let display (uc: FSharpUnionCase) = + uc.Fields[0].FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + + match payloadType unionCase with + | None -> ErasedUnionCaseTests.NotCaseTests [] + | Some myType -> + match tryUntestableKind com myType with + | Some kind -> ErasedUnionCaseTests.Untestable $"type '%s{display unionCase}' is %s{kind}" + | None -> + let myKey = tryRuntimeTypeTestKey com myType + + tdef.UnionCases + |> Seq.filter (fun uc -> uc.Name <> unionCase.Name) + |> Seq.fold + (fun acc other -> + match acc, payloadType other with + | ErasedUnionCaseTests.NotCaseTests macros, Some otherType -> + let isSubtype = passesTestOf com myType otherType + + if not isSubtype && (myKey.IsNone || myKey <> tryRuntimeTypeTestKey com otherType) then + acc + else + match FsUnionCase.CaseTest other with + | Some macro -> ErasedUnionCaseTests.NotCaseTests(macros @ [ macro ]) + | None -> + let reason = + if isSubtype then + $"'%s{display other}' is a subtype of '%s{display unionCase}'" + elif display unionCase = display other then + $"both have type '%s{display unionCase}'" + else + $"types '%s{display unionCase}' and '%s{display other}' look the same at runtime" + + ErasedUnionCaseTests.Ambiguous(other, reason) + | _ -> acc + ) + (ErasedUnionCaseTests.NotCaseTests []) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index a22a9766bb..ccf536935f 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -582,32 +582,83 @@ let private transformUnionCaseTest | ErasedUnionCase -> return "Cannot test erased union cases" |> addErrorAndReturnNull com ctx.InlinePath r | ErasedUnion(tdef, genArgs, rule) -> + let payloadType (uc: FSharpUnionCase) = + match uc.Fields.Count with + | 1 -> + let fi = uc.Fields[0] + + let typ = + if fi.FieldType.IsGenericParameter then + let name = genParamName fi.FieldType.GenericParameter + + let index = + tdef.GenericParameters |> Seq.findIndex (fun arg -> genParamName arg = name) + + genArgs[index] + else + fi.FieldType + + makeType ctx.GenericArgs typ |> Some + | _ -> None + + let supportsCaseTest = + match com.Options.Language with + | JavaScript + | TypeScript -> not (isReplacementCandidate (FsEnt.Ref tdef)) + | _ -> false + match unionCase.Fields.Count with | 0 -> return makeEqOp r unionExpr (transformStringEnum rule unionCase) BinaryEqual | 1 -> - let fi = unionCase.Fields[0] + let fableType = (payloadType unionCase).Value - let typ = - if fi.FieldType.IsGenericParameter then - let name = genParamName fi.FieldType.GenericParameter + let typeTest unionExpr = + Fable.Test(unionExpr, Fable.TypeTest fableType, r) - let index = - tdef.GenericParameters |> Seq.findIndex (fun arg -> genParamName arg = name) + let anyError = + $"Erased union case '%s{unionCase.Name}' is typed as 'obj' which cannot be tested at runtime (type test always evaluates to true). Use a more specific type or TypeScriptTaggedUnion instead." - genArgs[index] - else - fi.FieldType + let tests: Result<(Fable.Expr -> Fable.Expr) list, string> = + match FsUnionCase.CaseTest unionCase, fableType with + | _, Fable.Any when not supportsCaseTest -> Error anyError + | _, _ when not supportsCaseTest -> Ok [ typeTest ] + | Some macro, _ -> + let caseTest unionExpr = + emitExpr r Fable.Boolean [ unionExpr ] macro - let fableType = makeType ctx.GenericArgs typ - - match fableType with - | Fable.Any -> - return - $"Erased union case '%s{unionCase.Name}' is typed as 'obj' which cannot be tested at runtime (type test always evaluates to true). Use a more specific type or TypeScriptTaggedUnion instead." - |> addErrorAndReturnNull com ctx.InlinePath r - | _ -> - let kind = fableType |> Fable.TypeTest - return Fable.Test(unionExpr, kind, r) + if isUntestableType com fableType then + Ok [ caseTest ] + else + Ok [ typeTest; caseTest ] + | None, Fable.Any -> Error anyError + | None, _ -> + match deriveErasedUnionCaseTests com tdef unionCase payloadType with + | ErasedUnionCaseTests.Untestable reason -> + Error + $"Case '%s{unionCase.Name}' of '%s{tdef.DisplayName}' cannot be tested at runtime: %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}'." + | ErasedUnionCaseTests.Ambiguous(other, reason) -> + Error + $"Cases '%s{unionCase.Name}' and '%s{other.Name}' of '%s{tdef.DisplayName}' cannot be told apart at runtime: %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}' or '%s{other.Name}'." + | ErasedUnionCaseTests.NotCaseTests macros -> + macros + |> List.distinct + |> List.map (fun macro unionExpr -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})") + |> fun negated -> Ok(typeTest :: negated) + + match tests with + | Error msg -> return addErrorAndReturnNull com ctx.InlinePath r msg + | Ok [ test ] -> return test unionExpr + | Ok tests -> + let makeTest unionExpr = + tests + |> List.map (fun test -> test unionExpr) + |> List.reduce (fun acc test -> makeLogOp r acc test LogicalAnd) + + if canHaveSideEffects com unionExpr then + let ident = getIdentUniqueName com ctx "unionValue" |> makeTypedIdent unionExpr.Type + return Fable.Let(ident, unionExpr, makeTest (Fable.IdentExpr ident)) + else + return makeTest unionExpr | _ -> return "Erased unions with multiple cases cannot have more than one field: " @@ -2237,10 +2288,23 @@ let rec private getUsedRootNames (com: Compiler) (usedNames: Set) decls ) let private warnIgnoredCaseTests (com: FableCompiler) ctx (ent: FSharpEntity) = - if ent.IsFSharpUnion && not (hasAttrib Atts.tsTaggedUnion ent.Attributes) then + let reason = + if not ent.IsFSharpUnion || hasAttrib Atts.tsTaggedUnion ent.Attributes then + None + elif not (hasAttrib Atts.erase ent.Attributes) then + Some "it is only supported on TypeScriptTaggedUnion and Erase union cases" + else + match (com :> Compiler).Options.Language with + | JavaScript + | TypeScript -> None + | _ -> Some "on Erase union cases it only applies when targeting JavaScript or TypeScript" + + match reason with + | None -> () + | Some reason -> for uci in ent.UnionCases do if (FsUnionCase.CaseTest uci).IsSome then - WarningCodes.caseTestIgnored uci.Name + WarningCodes.caseTestIgnored uci.Name reason |> addWarningWithCode (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = diff --git a/src/Fable.Transforms/Global/WarningCodes.fs b/src/Fable.Transforms/Global/WarningCodes.fs index 73a25e0d86..b5b34e627d 100644 --- a/src/Fable.Transforms/Global/WarningCodes.fs +++ b/src/Fable.Transforms/Global/WarningCodes.fs @@ -109,9 +109,9 @@ let inlinedArgumentDoubleEvaluation (argName: string) = "FABLE0115", $"Inlined argument %s{argName} is being captured but is also used somewhere else. There's a risk of double evaluation." -/// A `CaseTest` attribute on a case of a union that is not a `TypeScriptTaggedUnion`. -let caseTestIgnored (caseName: string) = - "FABLE0116", $"CaseTest attribute on '%s{caseName}' is ignored: it is only supported on TypeScriptTaggedUnion cases" +/// A `CaseTest` attribute on a union case where Fable does not use it. +let caseTestIgnored (caseName: string) (reason: string) = + "FABLE0116", $"CaseTest attribute on '%s{caseName}' is ignored: %s{reason}" /// Rust: `String.Compare` and `String.CompareTo` are compiled as ordinal comparisons. let stringCompareIsOrdinal (methodName: string) = @@ -154,7 +154,7 @@ let knownCodes = baseCallIgnored "" nonPrimaryBaseConstructor inlinedArgumentDoubleEvaluation "" - caseTestIgnored "" + caseTestIgnored "" "" stringCompareIsOrdinal "" charSurrogateTestsAreFalse staticDoBindingsDisabled diff --git a/tests/Integration/Compiler/BehaviorWarningTests.fs b/tests/Integration/Compiler/BehaviorWarningTests.fs index 642f6a1234..c38f57e10f 100644 --- a/tests/Integration/Compiler/BehaviorWarningTests.fs +++ b/tests/Integration/Compiler/BehaviorWarningTests.fs @@ -104,7 +104,6 @@ orZero (Some(System.Random().Next())) |> ignore """ open Fable.Core -[] type Id = | [] Name of string | [] Slug of string diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index f570155bee..50adbae784 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -287,19 +287,18 @@ type MyClass() = |> Assert.Is.success |> ignore - testCase "CaseTest outside a TypeScriptTaggedUnion emits warning" <| fun _ -> + testCase "CaseTest on a regular union emits warning" <| fun _ -> let source = """ open Fable.Core -[] type Id = | [] Name of string | [] Slug of string """ compile source - |> Assert.Exists.warningWith "CaseTest attribute on 'Name' is ignored: it is only supported on TypeScriptTaggedUnion cases" - |> Assert.Exists.warningWith "CaseTest attribute on 'Slug' is ignored: it is only supported on TypeScriptTaggedUnion cases" + |> Assert.Exists.warningWith "CaseTest attribute on 'Name' is ignored: it is only supported on TypeScriptTaggedUnion and Erase union cases" + |> Assert.Exists.warningWith "CaseTest attribute on 'Slug' is ignored: it is only supported on TypeScriptTaggedUnion and Erase union cases" |> ignore testCase "CaseTest in a TypeScriptTaggedUnion does not emit warning" <| fun _ -> @@ -565,4 +564,144 @@ let isOk (s: Status) = s.IsOk compile source |> Assert.Exists.errorWith "Cases 'Ok' and 'Warn' of 'Status' share the tag value true and have the same fields. Add a CaseTest attribute to 'Ok' or 'Warn'." |> ignore + + testCase "Erased union case of interface type emits error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + +[] +type Shape = + | Circle of Circle + | Rect of Rect + +let isRect (s: Shape) = match s with Rect _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Case 'Rect' of 'Shape' cannot be tested at runtime: type 'Rect' is an interface that does not exist at runtime. Add a CaseTest attribute to 'Rect'." + |> ignore + + testCase "Erased union cases of the same type emit error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Id = + | Name of string + | Slug of string + +let isName (id: Id) = match id with Name _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Name' and 'Slug' of 'Id' cannot be told apart at runtime: both have type 'string'. Add a CaseTest attribute to 'Name' or 'Slug'." + |> ignore + + testCase "Erased union cases of types with the same runtime test emit error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Quantity = + | Count of int + | Amount of float + +let isCount (q: Quantity) = match q with Count _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Count' and 'Amount' of 'Quantity' cannot be told apart at runtime: types 'int' and 'float' look the same at runtime. Add a CaseTest attribute to 'Count' or 'Amount'." + |> ignore + + testCase "Erased union case whose type includes another case emits error" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = + member _.Kind = "animal" + +type Dog() = + inherit Animal() + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +let isAnimal (p: Pet) = match p with Animal _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Animal' and 'Dog' of 'Pet' cannot be told apart at runtime: 'Dog' is a subtype of 'Animal'. Add a CaseTest attribute to 'Animal' or 'Dog'." + |> ignore + + testCase "Erased union cases of distinct types do not emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Animal() = + member _.Kind = "animal" + +type Dog() = + inherit Animal() + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +[] +type Value = + | Str of string + | Num of float + | Flag of bool + | Many of float[] + +let isDog (p: Pet) = match p with Dog _ -> true | _ -> false +let describe (v: Value) = match v with Str _ -> "str" | Num _ -> "num" | Flag _ -> "flag" | Many _ -> "many" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Erased union cases with CaseTest do not emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +type Rect = + abstract width: float + +[] +type Shape = + | [] Circle of Circle + | [] Rect of Rect + +[] +type Id = + | [] Name of string + | Slug of string + +[] +type Raw = + | [] Obj of obj + | Text of string + +let describe (s: Shape) = match s with Circle _ -> "circle" | Rect _ -> "rect" +let kind (id: Id) = match id with Name _ -> "name" | Slug _ -> "slug" +let raw (r: Raw) = match r with Obj _ -> "obj" | Text _ -> "text" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore ] diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 051faf7637..ba96a9ee85 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -451,6 +451,32 @@ module TaggedUnion = | [] Off of flag: bool | [] Idle +module ErasedCaseTest = + type Circle = + abstract radius: float + + type Rect = + abstract width: float + + [] + type Shape = + | [] Circle of Circle + | [] Rect of Rect + + [] + type Id = + | [] Name of string + | Slug of string + | Count of int + + let circle r = + { new Circle with + member _.radius = r } + + let rect w = + { new Rect with + member _.width = w } + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -1279,6 +1305,22 @@ let tests = (TaggedUnion.NumberTag.Off false).IsOff |> equal true (TaggedUnion.NumberTag.Off false).IsOn |> equal false + testCase "Erased union cases of interface type use their CaseTest" <| fun () -> + let describe = function + | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius + | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + ErasedCaseTest.Shape.Circle (ErasedCaseTest.circle 1.5) |> describe |> equal "circle 1.5" + ErasedCaseTest.Shape.Rect (ErasedCaseTest.rect 2.5) |> describe |> equal "rect 2.5" + + testCase "Erased union cases of the same type use the CaseTest of the other case" <| fun () -> + let describe = function + | ErasedCaseTest.Id.Slug s -> "slug " + s + | ErasedCaseTest.Id.Name n -> "name " + n + | ErasedCaseTest.Id.Count c -> sprintf "count %i" c + ErasedCaseTest.Id.Name "n:x" |> describe |> equal "name n:x" + ErasedCaseTest.Id.Slug "s:y" |> describe |> equal "slug s:y" + ErasedCaseTest.Id.Count 3 |> describe |> equal "count 3" + #if FABLE_COMPILER testCase "Case testing with TS tagged unions whose cases share a tag works on raw JS objects" <| fun () -> let describeShared = function @@ -1324,6 +1366,18 @@ let tests = !!{| code = 2; flag = true |} |> describeNumber |> equal "on" !!{| code = 2; flag = false |} |> describeNumber |> equal "off" !!{| code = 3 |} |> describeNumber |> equal "idle" + let describeShape = function + | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius + | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + let describeId = function + | ErasedCaseTest.Id.Slug s -> "slug " + s + | ErasedCaseTest.Id.Name n -> "name " + n + | ErasedCaseTest.Id.Count c -> sprintf "count %i" c + !!{| radius = 3.0 |} |> describeShape |> equal "circle 3.0" + !!{| width = 4.0 |} |> describeShape |> equal "rect 4.0" + !!"n:raw" |> describeId |> equal "name n:raw" + !!"raw" |> describeId |> equal "slug raw" + !!7 |> describeId |> equal "count 7" #endif testCase "Pattern matching with StringEnum works" <| fun () -> From 3a2953087fd6749f211263b049d94eafd289472d Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 19:57:56 +0200 Subject: [PATCH 221/322] feat(js/ts): derive erased union tests from interfaces (#5133) --- src/Fable.Core/Fable.Core.Types.fs | 6 +- src/Fable.Transforms/FSharp2Fable.Util.fs | 190 ++++++++++++++---- src/Fable.Transforms/FSharp2Fable.fs | 43 ++-- .../Compiler/CompilerMessagesTests.fs | 94 ++++++++- tests/Js/Main/JsInteropTests.fs | 75 ++++++- 5 files changed, 340 insertions(+), 68 deletions(-) diff --git a/src/Fable.Core/Fable.Core.Types.fs b/src/Fable.Core/Fable.Core.Types.fs index 1d956680e1..9c1bcef475 100644 --- a/src/Fable.Core/Fable.Core.Types.fs +++ b/src/Fable.Core/Fable.Core.Types.fs @@ -107,9 +107,9 @@ type TypeScriptTaggedUnionAttribute(tagName: string, caseRules: CaseRules) = /// | Pending /// /// [<Erase>] -/// type Shape = -/// | [<CaseTest("'radius' in $0")>] Circle of Circle -/// | [<CaseTest("'width' in $0")>] Rect of Rect +/// type Id = +/// | [<CaseTest("$0.startsWith('n:')")>] Name of string +/// | Slug of string /// /// type CaseTestAttribute(test: string) = diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 449a74bb6f..870cd11405 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -3304,12 +3304,13 @@ module Util = | Try (tryGetIdentFromScope ctx r (Some typ)) expr, _ -> expr | _ -> getValueMemberRef v |> memberIdent com ctx r typ v - /// Runtime checks that tell apart TypeScriptTaggedUnion cases sharing a tag value + /// Runtime checks that tell apart union cases the tag or type test alone cannot [] - type TaggedUnionCaseTest = + type DerivedCaseTest = | HasField of name: string | LacksField of name: string | FieldOfType of name: string * typ: Fable.Type + | NotOfType of typ: Fable.Type | NotCaseTest of macro: string let private tryTaggedUnionTagKey (rule: CaseRules) (unionCase: FSharpUnionCase) = @@ -3391,11 +3392,8 @@ module Util = | Fable.DeclaredType(superRef, _), Fable.DeclaredType(subRef, _) -> isSubtypeOf com superRef.FullName subRef | _ -> false - let private tryDistinguishTaggedUnionCase - (com: Compiler) - (mine: TaggedUnionField list) - (other: TaggedUnionField list) - = + /// A required field that only one side has + let private tryDistinguishByPresence (mine: TaggedUnionField list) (other: TaggedUnionField list) = let isRequired (f: TaggedUnionField) = match f.Type with | Fable.Option _ -> false @@ -3407,13 +3405,20 @@ module Util = let byPresence = mine |> List.tryFind (fun f -> isRequired f && not (hasName f.Name other)) - |> Option.map (fun f -> TaggedUnionCaseTest.HasField f.Name) + |> Option.map (fun f -> DerivedCaseTest.HasField f.Name) let byAbsence () = other |> List.tryFind (fun f -> isRequired f && not (hasName f.Name mine)) - |> Option.map (fun f -> TaggedUnionCaseTest.LacksField f.Name) + |> Option.map (fun f -> DerivedCaseTest.LacksField f.Name) + + byPresence |> Option.orElseWith byAbsence + let private tryDistinguishTaggedUnionCase + (com: Compiler) + (mine: TaggedUnionField list) + (other: TaggedUnionField list) + = let byType () = mine |> List.tryPick (fun f -> @@ -3426,11 +3431,11 @@ module Util = && not (passesTestOf com f.Type o.Type) && not (passesTestOf com o.Type f.Type) -> - Some(TaggedUnionCaseTest.FieldOfType(f.Name, f.Type)) + Some(DerivedCaseTest.FieldOfType(f.Name, f.Type)) | _ -> None ) - byPresence |> Option.orElseWith byAbsence |> Option.orElseWith byType + tryDistinguishByPresence mine other |> Option.orElseWith byType /// Why a type is known to have no runtime test, None when it has one or when this is unknown let private tryUntestableKind (com: Compiler) (typ: Fable.Type) = @@ -3483,12 +3488,12 @@ module Util = "cannot be told apart at runtime: " + reason /// One check per other case with the same tag value, or the case the fields cannot exclude - let deriveTaggedUnionCaseTests + let deriveDerivedCaseTests (com: Compiler) (tdef: FSharpEntity) (rule: CaseRules) (unionCase: FSharpUnionCase) - : Result + : Result = let fieldsOf (uc: FSharpUnionCase) = uc.Fields @@ -3516,7 +3521,7 @@ module Util = let otherFields = fieldsOf other let byCaseTest () = - FsUnionCase.CaseTest other |> Option.map TaggedUnionCaseTest.NotCaseTest + FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest match tryDistinguishTaggedUnionCase com myFields otherFields @@ -3528,14 +3533,71 @@ module Util = (Ok []) |> Result.map List.distinct + /// Required members of an interface and the interfaces it inherits, with the names they have at runtime + let private tryInterfaceMembers (com: Compiler) (typ: Fable.Type) : TaggedUnionField list option = + let hasRuntimeName (m: Fable.MemberFunctionOrValue) = + m.Attributes + |> Seq.forall (fun att -> + match att.Entity.FullName with + | Naming.StartsWith Atts.emit _ + | Naming.StartsWith Atts.import _ + | Atts.global_ -> false + | _ -> true + ) + + let isPlainInterface (ent: Fable.Entity) = + ent.IsInterface + && not (hasAttribute Atts.mangle ent) + && not (ent.FullName.StartsWith("System.", StringComparison.Ordinal)) + + let membersOf (ent: Fable.Entity) = + ent.MembersFunctionsAndValues + |> Seq.filter (fun m -> + m.IsInstance + && not m.IsConstructor + && not m.IsGetter + && not m.IsSetter + && hasRuntimeName m + ) + |> Seq.map (fun m -> + { + Name = + if m.IsProperty then + m.DisplayName + else + m.CompiledName + Type = + if m.IsProperty then + m.ReturnParameter.Type + else + Fable.Any + Display = m.DisplayName + } + ) + + match typ with + | Fable.DeclaredType(entRef, _) -> + match com.TryGetEntity entRef with + | Some ent when isPlainInterface ent -> + ent.AllInterfaces + |> Seq.choose (fun i -> com.TryGetEntity i.Entity) + |> Seq.filter isPlainInterface + |> Seq.collect membersOf + |> Seq.append (membersOf ent) + |> Seq.distinctBy (fun f -> f.Name) + |> List.ofSeq + |> Some + | _ -> None + | _ -> None + /// Checks added to the type test of an erased union case, or why the type test is not enough [] type ErasedUnionCaseTests = - | NotCaseTests of macros: string list + | Tests of DerivedCaseTest list | Untestable of reason: string | Ambiguous of other: FSharpUnionCase * reason: string - /// Negated CaseTests of the other cases whose values pass this case's type test + /// Checks that exclude the other cases from this one: negated type tests, interface members, or negated CaseTests let deriveErasedUnionCaseTests (com: Compiler) (tdef: FSharpEntity) @@ -3546,37 +3608,87 @@ module Util = let display (uc: FSharpUnionCase) = uc.Fields[0].FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + let describeUntestable (uc: FSharpUnionCase) (typ: Fable.Type) kind = + match typ with + | Fable.Any -> $"type '%s{display uc}' can hold any value" + | _ -> $"type '%s{display uc}' is %s{kind}" + + let byCaseTest (other: FSharpUnionCase) = + FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest + match payloadType unionCase with - | None -> ErasedUnionCaseTests.NotCaseTests [] + | None -> ErasedUnionCaseTests.Tests [] | Some myType -> - match tryUntestableKind com myType with - | Some kind -> ErasedUnionCaseTests.Untestable $"type '%s{display unionCase}' is %s{kind}" - | None -> + let myKind = tryUntestableKind com myType + + let myMembers = + if myKind.IsSome then + tryInterfaceMembers com myType + else + None + + match myKind, myMembers with + | Some kind, None -> ErasedUnionCaseTests.Untestable(describeUntestable unionCase myType kind) + | _ -> let myKey = tryRuntimeTypeTestKey com myType + let excludeByType (other: FSharpUnionCase) (otherType: Fable.Type) = + let isSubtype = passesTestOf com myType otherType + + let sameKey = myKey.IsSome && myKey = tryRuntimeTypeTestKey com otherType + + if not isSubtype && not sameKey then + Ok [] + else + match byCaseTest other with + | Some test -> Ok [ test ] + | None when isSubtype -> Error $"'%s{display other}' is a subtype of '%s{display unionCase}'" + | None when display unionCase = display other -> Error $"both have type '%s{display unionCase}'" + | None -> + Error $"types '%s{display unionCase}' and '%s{display other}' look the same at runtime" + + let excludeByMembers (mine: TaggedUnionField list) (other: FSharpUnionCase) (otherType: Fable.Type) = + match tryUntestableKind com otherType with + | None -> Ok [ DerivedCaseTest.NotOfType otherType ] + | Some otherKind -> + let theirs = tryInterfaceMembers com otherType + + match + theirs + |> Option.bind (tryDistinguishByPresence mine) + |> Option.orElseWith (fun () -> byCaseTest other) + with + | Some test -> Ok [ test ] + | None when theirs.IsSome -> + Error + $"interfaces '%s{display unionCase}' and '%s{display other}' have no required member that tells them apart" + | None -> Error(describeUntestable other otherType otherKind) + + let exclude = + match myMembers with + | Some mine -> excludeByMembers mine + | None -> excludeByType + tdef.UnionCases |> Seq.filter (fun uc -> uc.Name <> unionCase.Name) |> Seq.fold (fun acc other -> match acc, payloadType other with - | ErasedUnionCaseTests.NotCaseTests macros, Some otherType -> - let isSubtype = passesTestOf com myType otherType - - if not isSubtype && (myKey.IsNone || myKey <> tryRuntimeTypeTestKey com otherType) then - acc - else - match FsUnionCase.CaseTest other with - | Some macro -> ErasedUnionCaseTests.NotCaseTests(macros @ [ macro ]) - | None -> - let reason = - if isSubtype then - $"'%s{display other}' is a subtype of '%s{display unionCase}'" - elif display unionCase = display other then - $"both have type '%s{display unionCase}'" - else - $"types '%s{display unionCase}' and '%s{display other}' look the same at runtime" - - ErasedUnionCaseTests.Ambiguous(other, reason) + | ErasedUnionCaseTests.Tests tests, Some otherType -> + match exclude other otherType with + | Ok more -> ErasedUnionCaseTests.Tests(tests @ more) + | Error reason -> ErasedUnionCaseTests.Ambiguous(other, reason) | _ -> acc ) - (ErasedUnionCaseTests.NotCaseTests []) + (ErasedUnionCaseTests.Tests []) + |> function + | ErasedUnionCaseTests.Tests tests -> + tests + |> List.distinct + |> List.sortBy ( + function + | DerivedCaseTest.NotOfType _ -> 0 + | _ -> 1 + ) + |> ErasedUnionCaseTests.Tests + | result -> result diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index ccf536935f..f12cda7961 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -566,6 +566,18 @@ let private transformDelegate com ctx (delegateType: FSharpType) expr = | _ -> return expr } +let private makeDerivedCaseTest r (unionExpr: Fable.Expr) (test: DerivedCaseTest) = + match test with + | DerivedCaseTest.HasField name -> emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "$0 in $1" + | DerivedCaseTest.LacksField name -> emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "!($0 in $1)" + | DerivedCaseTest.FieldOfType(name, typ) -> + let field = + Fable.Get(unionExpr, Fable.FieldInfo.Create(name, fieldType = typ), typ, r) + + Fable.Test(field, Fable.TypeTest typ, r) + | DerivedCaseTest.NotOfType typ -> makeUnOp r Fable.Boolean (Fable.Test(unionExpr, Fable.TypeTest typ, r)) UnaryNot + | DerivedCaseTest.NotCaseTest macro -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})" + let private transformUnionCaseTest (com: IFableCompiler) (ctx: Context) @@ -639,14 +651,18 @@ let private transformUnionCaseTest | ErasedUnionCaseTests.Ambiguous(other, reason) -> Error $"Cases '%s{unionCase.Name}' and '%s{other.Name}' of '%s{tdef.DisplayName}' cannot be told apart at runtime: %s{reason}. Add a CaseTest attribute to '%s{unionCase.Name}' or '%s{other.Name}'." - | ErasedUnionCaseTests.NotCaseTests macros -> - macros - |> List.distinct - |> List.map (fun macro unionExpr -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})") - |> fun negated -> Ok(typeTest :: negated) + | ErasedUnionCaseTests.Tests derived -> + let derived = + derived |> List.map (fun test unionExpr -> makeDerivedCaseTest r unionExpr test) + + if isUntestableType com fableType then + Ok derived + else + Ok(typeTest :: derived) match tests with | Error msg -> return addErrorAndReturnNull com ctx.InlinePath r msg + | Ok [] -> return makeBoolConst true | Ok [ test ] -> return test unionExpr | Ok tests -> let makeTest unionExpr = @@ -683,25 +699,12 @@ let private transformUnionCaseTest let getTag = Fable.Get(unionExpr, Fable.FieldInfo.Create(tagName), value.Type, r) makeEqOp r getTag value BinaryEqual - let makeDerivedTest unionExpr test = - match test with - | TaggedUnionCaseTest.HasField name -> - emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "$0 in $1" - | TaggedUnionCaseTest.LacksField name -> - emitExpr r Fable.Boolean [ makeStrConst name; unionExpr ] "!($0 in $1)" - | TaggedUnionCaseTest.FieldOfType(name, typ) -> - let field = - Fable.Get(unionExpr, Fable.FieldInfo.Create(name, fieldType = typ), typ, r) - - Fable.Test(field, Fable.TypeTest typ, r) - | TaggedUnionCaseTest.NotCaseTest macro -> emitExpr r Fable.Boolean [ unionExpr ] $"!(%s{macro})" - let caseTests: (Fable.Expr -> Fable.Expr) list = match FsUnionCase.CaseTest unionCase with | Some macro -> [ fun unionExpr -> emitExpr r Fable.Boolean [ unionExpr ] macro ] | None -> - match deriveTaggedUnionCaseTests com tdef rule unionCase with - | Ok tests -> tests |> List.map (fun test unionExpr -> makeDerivedTest unionExpr test) + match deriveDerivedCaseTests com tdef rule unionCase with + | Ok tests -> tests |> List.map (fun test unionExpr -> makeDerivedCaseTest r unionExpr test) | Error(other, reason) -> let tag = taggedUnionTagDisplay rule unionCase diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 50adbae784..6984017781 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -565,7 +565,7 @@ let isOk (s: Status) = s.IsOk |> Assert.Exists.errorWith "Cases 'Ok' and 'Warn' of 'Status' share the tag value true and have the same fields. Add a CaseTest attribute to 'Ok' or 'Warn'." |> ignore - testCase "Erased union case of interface type emits error" <| fun _ -> + testCase "Erased union cases of interface type are told apart by their members" <| fun _ -> let source = """ open Fable.Core @@ -575,6 +575,53 @@ type Circle = type Rect = abstract width: float + abstract area: unit -> float + +type Animal = + abstract name: string + +type Dog = + inherit Animal + abstract bark: string + +type Marker = interface end + +[] +type Shape = + | Circle of Circle + | Rect of Rect + | Count of int + | Label of string + +[] +type Pet = + | Animal of Animal + | Dog of Dog + +[] +type Tagged = + | Mark of Marker + | Name of string + +let describe (s: Shape) = match s with Circle _ -> "circle" | Rect _ -> "rect" | Count _ -> "count" | Label _ -> "label" +let pet (p: Pet) = match p with Animal _ -> "animal" | Dog _ -> "dog" +let tagged (t: Tagged) = match t with Mark _ -> "mark" | Name _ -> "name" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Erased union cases of interfaces with the same members emit error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract size: float + +type Rect = + abstract size: float + abstract label: string option [] type Shape = @@ -584,7 +631,50 @@ type Shape = let isRect (s: Shape) = match s with Rect _ -> true | _ -> false """ compile source - |> Assert.Exists.errorWith "Case 'Rect' of 'Shape' cannot be tested at runtime: type 'Rect' is an interface that does not exist at runtime. Add a CaseTest attribute to 'Rect'." + |> Assert.Exists.errorWith "Cases 'Rect' and 'Circle' of 'Shape' cannot be told apart at runtime: interfaces 'Rect' and 'Circle' have no required member that tells them apart. Add a CaseTest attribute to 'Rect' or 'Circle'." + |> ignore + + testCase "Erased union case of mangled interface type emits error" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Circle = + abstract radius: float + +[] +type Shape = + | Circle of Circle + | Count of int + +let isCircle (s: Shape) = match s with Circle _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Case 'Circle' of 'Shape' cannot be tested at runtime: type 'Circle' is an interface that does not exist at runtime. Add a CaseTest attribute to 'Circle'." + |> ignore + + testCase "Erased union case of interface type next to a Pojo case emits error" <| fun _ -> + let source = + """ +open Fable.Core + +type Circle = + abstract radius: float + +[] +type Options(name: string) = + member val name = name + +[] +type Shape = + | Circle of Circle + | Opts of Options + +let isCircle (s: Shape) = match s with Circle _ -> true | _ -> false +""" + compile source + |> Assert.Exists.errorWith "Cases 'Circle' and 'Opts' of 'Shape' cannot be told apart at runtime: type 'Options' is a Pojo that is a plain object at runtime. Add a CaseTest attribute to 'Circle' or 'Opts'." |> ignore testCase "Erased union cases of the same type emit error" <| fun _ -> diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index ba96a9ee85..4fdd65d4b5 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -458,10 +458,32 @@ module ErasedCaseTest = type Rect = abstract width: float + type Animal = + abstract name: string + + type Dog = + inherit Animal + abstract bark: string + + type Kinded = + abstract kind: string + [] type Shape = - | [] Circle of Circle - | [] Rect of Rect + | Circle of Circle + | Rect of Rect + | Count of int + | Label of string + + [] + type Pet = + | Animal of Animal + | Dog of Dog + + [] + type Tagged = + | [] A of Kinded + | B of Kinded [] type Id = @@ -477,6 +499,19 @@ module ErasedCaseTest = { new Rect with member _.width = w } + let animal n = + { new Animal with + member _.name = n } + + let dog n b = + { new Dog with + member _.name = n + member _.bark = b } + + let kinded k = + { new Kinded with + member _.kind = k } + #if FABLE_COMPILER module PojoDefinedByConsArgs = [] @@ -1305,12 +1340,30 @@ let tests = (TaggedUnion.NumberTag.Off false).IsOff |> equal true (TaggedUnion.NumberTag.Off false).IsOn |> equal false - testCase "Erased union cases of interface type use their CaseTest" <| fun () -> + testCase "Erased union cases of interface type are told apart by their members" <| fun () -> let describe = function | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + | ErasedCaseTest.Shape.Count n -> sprintf "count %i" n + | ErasedCaseTest.Shape.Label l -> "label " + l ErasedCaseTest.Shape.Circle (ErasedCaseTest.circle 1.5) |> describe |> equal "circle 1.5" ErasedCaseTest.Shape.Rect (ErasedCaseTest.rect 2.5) |> describe |> equal "rect 2.5" + ErasedCaseTest.Shape.Count 3 |> describe |> equal "count 3" + ErasedCaseTest.Shape.Label "x" |> describe |> equal "label x" + + testCase "Erased union cases of inherited interface types are told apart by their members" <| fun () -> + let describe = function + | ErasedCaseTest.Pet.Animal a -> "animal " + a.name + | ErasedCaseTest.Pet.Dog d -> "dog " + d.name + " " + d.bark + ErasedCaseTest.Pet.Animal (ErasedCaseTest.animal "cat") |> describe |> equal "animal cat" + ErasedCaseTest.Pet.Dog (ErasedCaseTest.dog "rex" "woof") |> describe |> equal "dog rex woof" + + testCase "Erased union cases of interface type use the CaseTest of the other case" <| fun () -> + let describe = function + | ErasedCaseTest.Tagged.B k -> "b " + k.kind + | ErasedCaseTest.Tagged.A k -> "a " + k.kind + ErasedCaseTest.Tagged.A (ErasedCaseTest.kinded "a") |> describe |> equal "a a" + ErasedCaseTest.Tagged.B (ErasedCaseTest.kinded "b") |> describe |> equal "b b" testCase "Erased union cases of the same type use the CaseTest of the other case" <| fun () -> let describe = function @@ -1369,12 +1422,26 @@ let tests = let describeShape = function | ErasedCaseTest.Shape.Circle c -> sprintf "circle %.1f" c.radius | ErasedCaseTest.Shape.Rect r -> sprintf "rect %.1f" r.width + | ErasedCaseTest.Shape.Count n -> sprintf "count %i" n + | ErasedCaseTest.Shape.Label l -> "label " + l + let describePet = function + | ErasedCaseTest.Pet.Animal a -> "animal " + a.name + | ErasedCaseTest.Pet.Dog d -> "dog " + d.name + " " + d.bark + let describeTagged = function + | ErasedCaseTest.Tagged.B k -> "b " + k.kind + | ErasedCaseTest.Tagged.A k -> "a " + k.kind let describeId = function | ErasedCaseTest.Id.Slug s -> "slug " + s | ErasedCaseTest.Id.Name n -> "name " + n | ErasedCaseTest.Id.Count c -> sprintf "count %i" c !!{| radius = 3.0 |} |> describeShape |> equal "circle 3.0" - !!{| width = 4.0 |} |> describeShape |> equal "rect 4.0" + !!{| width = 4.0; area = fun () -> 1.0 |} |> describeShape |> equal "rect 4.0" + !!5 |> describeShape |> equal "count 5" + !!"raw" |> describeShape |> equal "label raw" + !!{| name = "cat" |} |> describePet |> equal "animal cat" + !!{| name = "rex"; bark = "woof" |} |> describePet |> equal "dog rex woof" + !!{| kind = "a" |} |> describeTagged |> equal "a a" + !!{| kind = "b" |} |> describeTagged |> equal "b b" !!"n:raw" |> describeId |> equal "name n:raw" !!"raw" |> describeId |> equal "slug raw" !!7 |> describeId |> equal "count 7" From b775276eb843c5bf6533424b325eef25940abcaa Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 20:38:05 +0200 Subject: [PATCH 222/322] fix(all): serialize output directory deduplication (#5143) --- src/Fable.Cli/Main.fs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 3914bedb27..f021cdaa45 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -1025,7 +1025,16 @@ type State = // as some OS use case insensitive paths let importDir = importDir.ToLower() - this.DeduplicateDic.GetOrAdd(importDir, (fun _ -> set this.DeduplicateDic.Values |> addTargetDir)) + lock + this.DeduplicateDic + (fun () -> + match this.DeduplicateDic.TryGetValue(importDir) with + | true, targetDir -> targetDir + | false, _ -> + let targetDir = set this.DeduplicateDic.Values |> addTargetDir + this.DeduplicateDic[importDir] <- targetDir + targetDir + ) } static member Create(cliArgs, ?watchDelay, ?recompileAllFiles) = From 09d066146f662e331d1b73c0ba60f2a0ed935fbc Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 20:57:43 +0200 Subject: [PATCH 223/322] fix(all): report compiler agent failures instead of hanging (#5144) --- src/Fable.Cli/Main.fs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index f021cdaa45..10fade4b05 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -569,6 +569,8 @@ type FableCompilerState = } and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fableProj: Project) = + let activeReplyChannel: ReplyChannel option ref = ref None + let agent = MailboxProcessor.Start(fun agent -> let postTo toMsg work = @@ -677,6 +679,8 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl isTriggeredByDependency, getSignatureDependents, replyChannel) -> + activeReplyChannel.Value <- Some replyChannel + let state = FableCompilerState.Create( state.FableProj, @@ -873,6 +877,12 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl FableCompilerState.Create(fableProj, [||]) |> loop ) + do + agent.Error.Add(fun e -> + activeReplyChannel.Value + |> Option.iter (fun channel -> Error e |> channel.Reply) + ) + member _.GetFableProject() = agent.PostAndAsyncReply(GetFableProject) From 0529fcf8b57cc5a6aca0a92aae5f444e53ea0f28 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 21:40:13 +0200 Subject: [PATCH 224/322] fix(all): refresh unchanged outputs so up-to-date skip works (#5145) --- src/Fable.Cli/Pipeline.fs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index eaf00773b8..638cb3b0e7 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -61,7 +61,10 @@ type Stream = return! areStreamsEqual memoryStream fileStream } - static member WriteToFileIfChanged(memoryStream: IO.Stream, targetPath: string) : Async = + static member WriteToFileIfChanged + (memoryStream: IO.Stream, targetPath: string, refreshUnchanged: bool) + : Async + = async { if memoryStream.Length = 0 then return false @@ -75,7 +78,11 @@ type Stream = else match! Stream.IsEqualToFile(memoryStream, targetPath) with | false -> return! Stream.WriteToFile(memoryStream, targetPath) - | true -> return false + | true -> + if refreshUnchanged then + IO.File.SetLastWriteTimeUtc(targetPath, DateTime.UtcNow) + + return false } module Js = @@ -102,7 +109,8 @@ module Js = do! stream.FlushAsync() |> Async.AwaitTask - let! written = Stream.WriteToFileIfChanged(memoryStream, targetPath) + let! written = + Stream.WriteToFileIfChanged(memoryStream, targetPath, not cliArgs.IsWatch) if written && cliArgs.SourceMaps then use fs = IO.File.Open(targetPath + ".map", IO.FileMode.Create) From 2be66611d884cd564fa3f7b06fd4a015add1cefd Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 22 Oct 2026 21:47:56 +0200 Subject: [PATCH 225/322] fix(js/ts): inline interface cases of TypeScriptTaggedUnion (#5146) --- src/Fable.Build/Test/Integration.fs | 1 + src/Fable.Transforms/Babel/Fable2Babel.fs | 29 +++-- src/Fable.Transforms/FSharp2Fable.Util.fs | 109 ++++++++++-------- src/Fable.Transforms/FSharp2Fable.fs | 50 ++++++-- src/Fable.Transforms/Transforms.Util.fs | 10 ++ .../Compiler/CompilerMessagesTests.fs | 72 ++++++++++++ .../Integration/CompilationTests.fs | 18 ++- .../tsTaggedUnionInterface/TaggedUnion.fs | 25 ++++ .../TaggedUnion.jsx.expected | 19 +++ .../tsTaggedUnionInterface.fsproj | 16 +++ .../tsTaggedUnionInterfaceTs/TaggedUnion.fs | 25 ++++ .../TaggedUnion.ts.expected | 35 ++++++ .../tsTaggedUnionInterfaceTs.fsproj | 16 +++ tests/Js/Main/JsInteropTests.fs | 94 +++++++++++++++ tests/TypeScript/LiteTsInteropTests.fs | 32 +++++ 15 files changed, 475 insertions(+), 76 deletions(-) create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected create mode 100644 tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj diff --git a/src/Fable.Build/Test/Integration.fs b/src/Fable.Build/Test/Integration.fs index 0b74733140..7aa61f41f8 100644 --- a/src/Fable.Build/Test/Integration.fs +++ b/src/Fable.Build/Test/Integration.fs @@ -37,6 +37,7 @@ let private testProjectConfig (projectDirName: string) (configuration: string op let handle (args: string list) = BuildFableLibraryJavaScript().Run() + BuildFableLibraryTypeScript().Run() Command.Run("dotnet", "run -c Release", workingDirectory = integrationProjectDir) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 87cc941525..431293cc85 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -922,16 +922,16 @@ module Annotation = AbstractMember.abstractProperty (prop, tagType, isComputed = isComputed) - match uci.UnionCaseFields with - // | [ field ] when field.Name = "Item" -> - // IntersectionTypeAnnotation - // [| - // field.FieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx - // ObjectTypeAnnotation [| tagMember |] - // |] - | fields -> + match tryInlinedTaggedUnionCaseType com uci with + | Some fieldType -> + IntersectionTypeAnnotation + [| + fieldType |> resolveInlineType genArgs |> makeFieldAnnotation com ctx + ObjectTypeAnnotation [| tagMember |] + |] + | None -> let names, types = - fields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip + uci.UnionCaseFields |> List.map (fun fi -> fi.Name, fi.FieldType) |> List.unzip makeAnonymousRecordTypeAnnotation com ctx (List.toArray names) types |> function @@ -1631,10 +1631,17 @@ module Util = match e.Type with | Fable.DeclaredType(sourceEntRef, _) when com.IsTypeScript -> - let sourceEntRef = com.GetEntity(sourceEntRef) + let sourceEnt = com.GetEntity(sourceEntRef) // Because we use a wrapper type for multi-case unions, TypeScript // won't automatically cast them to implementing interfaces - if sourceEntRef.IsFSharpUnion && List.isMultiple sourceEntRef.UnionCases then + let fromUnion = sourceEnt.IsFSharpUnion && List.isMultiple sourceEnt.UnionCases + + // The interface value of an inlined case is not assignable to the literal tag type + let toTaggedUnion = + com.TryGetEntity(entRef) + |> Option.exists (fun ent -> hasAttribute Atts.tsTaggedUnion ent.Attributes) + + if fromUnion || toTaggedUnion then AsExpression(jsExpr, makeTypeAnnotation com ctx typ) else jsExpr diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 870cd11405..f9f09bec8b 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -144,10 +144,6 @@ type FsUnionCase(uci: FSharpUnionCase) = |> Helpers.tryFindAttrib Atts.caseTest |> Option.map (fun (att: FSharpAttribute) -> att.ConstructorArguments[0] |> snd |> string) - // static member HasNamedFields(uci: FSharpUnionCase) = - // not (uci.Fields.Count = 1 && uci.Fields[0].Name = "Item") - // true - interface Fable.UnionCase with member _.Name = uci.Name member _.FullName = FsUnionCase.FullName uci @@ -3487,52 +3483,6 @@ module Util = "cannot be told apart at runtime: " + reason - /// One check per other case with the same tag value, or the case the fields cannot exclude - let deriveDerivedCaseTests - (com: Compiler) - (tdef: FSharpEntity) - (rule: CaseRules) - (unionCase: FSharpUnionCase) - : Result - = - let fieldsOf (uc: FSharpUnionCase) = - uc.Fields - |> Seq.map (fun fi -> - { - Name = FsField.FSharpFieldName fi - Type = makeType Map.empty fi.FieldType - Display = fi.FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) - } - ) - |> List.ofSeq - - match tryTaggedUnionTagKey rule unionCase with - | None -> Ok [] - | Some myTag -> - let myFields = fieldsOf unionCase - - tdef.UnionCases - |> Seq.filter (fun uc -> uc.Name <> unionCase.Name && tryTaggedUnionTagKey rule uc = Some myTag) - |> Seq.fold - (fun acc other -> - match acc with - | Error _ -> acc - | Ok tests -> - let otherFields = fieldsOf other - - let byCaseTest () = - FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest - - match - tryDistinguishTaggedUnionCase com myFields otherFields - |> Option.orElseWith byCaseTest - with - | Some test -> Ok(tests @ [ test ]) - | None -> Error(other, explainIndistinguishable com myFields otherFields) - ) - (Ok []) - |> Result.map List.distinct - /// Required members of an interface and the interfaces it inherits, with the names they have at runtime let private tryInterfaceMembers (com: Compiler) (typ: Fable.Type) : TaggedUnionField list option = let hasRuntimeName (m: Fable.MemberFunctionOrValue) = @@ -3590,6 +3540,65 @@ module Util = | _ -> None | _ -> None + /// The interface type a TypeScriptTaggedUnion case compiles to: a single unnamed field of an interface type + let tryInlinedTaggedUnionCase (com: Compiler) (unionCase: FSharpUnionCase) = + tryInlinedTaggedUnionCaseType com (FsUnionCase unionCase) + + /// Whether the interface of an inlined TypeScriptTaggedUnion case declares the tag member, None when the case is not inlined or the members are unknown + let inlinedTaggedUnionCaseHasTag (com: Compiler) (tagName: string) (unionCase: FSharpUnionCase) = + tryInlinedTaggedUnionCase com unionCase + |> Option.bind (tryInterfaceMembers com) + |> Option.map (List.exists (fun m -> m.Name = tagName)) + + /// One check per other case with the same tag value, or the case the fields cannot exclude + let deriveDerivedCaseTests + (com: Compiler) + (tdef: FSharpEntity) + (rule: CaseRules) + (unionCase: FSharpUnionCase) + : Result + = + let fieldsOf (uc: FSharpUnionCase) = + match tryInlinedTaggedUnionCase com uc with + | Some typ -> tryInterfaceMembers com typ |> Option.defaultValue [] + | None -> + uc.Fields + |> Seq.map (fun fi -> + { + Name = FsField.FSharpFieldName fi + Type = makeType Map.empty fi.FieldType + Display = fi.FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + } + ) + |> List.ofSeq + + match tryTaggedUnionTagKey rule unionCase with + | None -> Ok [] + | Some myTag -> + let myFields = fieldsOf unionCase + + tdef.UnionCases + |> Seq.filter (fun uc -> uc.Name <> unionCase.Name && tryTaggedUnionTagKey rule uc = Some myTag) + |> Seq.fold + (fun acc other -> + match acc with + | Error _ -> acc + | Ok tests -> + let otherFields = fieldsOf other + + let byCaseTest () = + FsUnionCase.CaseTest other |> Option.map DerivedCaseTest.NotCaseTest + + match + tryDistinguishTaggedUnionCase com myFields otherFields + |> Option.orElseWith byCaseTest + with + | Some test -> Ok(tests @ [ test ]) + | None -> Error(other, explainIndistinguishable com myFields otherFields) + ) + (Ok []) + |> Result.map List.distinct + /// Checks added to the type test of an erased union case, or why the type test is not enough [] type ErasedUnionCaseTests = diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index f12cda7961..b75db8460c 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -110,8 +110,8 @@ let private transformNewUnion com ctx r fsType (unionCase: FSharpUnionCase) (arg | argExprs -> makeTuple r false argExprs | TypeScriptTaggedUnion(_, _, tagName, rule) -> - match argExprs with - // | [ argExpr ] when not (FsUnionCase.HasNamedFields unionCase) -> argExpr + match tryInlinedTaggedUnionCase com unionCase, argExprs with + | Some _, [ argExpr ] -> Fable.TypeCast(argExpr, makeType ctx.GenericArgs fsType) | _ -> let isCompiledValue, tagExpr = match FsUnionCase.CompiledValue unionCase with @@ -1429,16 +1429,16 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return Fable.Get(unionExpr, Fable.TupleIndex index, fieldType, r) | TypeScriptTaggedUnion _ -> - // if not (FsUnionCase.HasNamedFields unionCase) then - // return unionExpr - // else - let kind = - Fable.FieldInfo.Create( - FsField.FSharpFieldName field, - fieldType = makeType Map.empty field.FieldType - ) + match tryInlinedTaggedUnionCase com unionCase with + | Some _ -> return Fable.TypeCast(unionExpr, fieldType) + | None -> + let kind = + Fable.FieldInfo.Create( + FsField.FSharpFieldName field, + fieldType = makeType Map.empty field.FieldType + ) - return Fable.Get(unionExpr, kind, fieldType, r) + return Fable.Get(unionExpr, kind, fieldType, r) | StringEnum _ -> return "StringEnum types cannot have fields" @@ -2310,12 +2310,40 @@ let private warnIgnoredCaseTests (com: FableCompiler) ctx (ent: FSharpEntity) = WarningCodes.caseTestIgnored uci.Name reason |> addWarningWithCode (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) +/// Values built in F# only carry the tag if the interface of an inlined case declares the tag member +let private checkInlinedTaggedUnionCases (com: FableCompiler) ctx (ent: FSharpEntity) = + let tagName = + if ent.IsFSharpUnion then + ent.Attributes + |> tryFindAttrib Atts.tsTaggedUnion + |> Option.bind (fun att -> + match Seq.tryHead att.ConstructorArguments with + | Some(_, (:? string as name)) -> Some name + | _ -> None + ) + else + None + + match tagName with + | None -> () + | Some tagName -> + for uci in ent.UnionCases do + match inlinedTaggedUnionCaseHasTag com tagName uci with + | Some false -> + let typeName = + uci.Fields[0].FieldType.Format(FSharpDisplayContext.Empty.WithShortTypeNames(true)) + + $"Case '%s{uci.Name}' of '%s{ent.DisplayName}' compiles to its '%s{typeName}' value, so the interface must declare a '%s{tagName}' member to carry the tag. Add the member, or name the field to compile the case as an object holding the tag and the field." + |> addError (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + | _ -> () + let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = fsDecls |> List.collect (fun fsDecl -> match fsDecl with | FSharpImplementationFileDeclaration.Entity(fsEnt, sub) -> warnIgnoredCaseTests com ctx fsEnt + checkInlinedTaggedUnionCases com ctx fsEnt match sub with | [] when isIgnoredLeafEntity fsEnt -> [] diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 5fa5235db0..1572a850d8 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -947,6 +947,16 @@ module AST = | _ -> ValueNone | _ -> ValueNone + /// The field type of a TypeScriptTaggedUnion case that compiles to its value: a single unnamed field of an interface type + let tryInlinedTaggedUnionCaseType (com: Compiler) (uci: UnionCase) = + match uci.UnionCaseFields with + | [ field ] when field.Name = "Item" -> + match field.FieldType with + | DeclaredType(entRef, _) when com.TryGetEntity(entRef) |> Option.exists _.IsInterface -> + Some field.FieldType + | _ -> None + | _ -> None + // TODO: Improve this, see https://github.com/fable-compiler/Fable/issues/1659#issuecomment-445071965 // This is mainly used for inlining so a computation or a reference to a mutable value are understood // as a side effects too (because we don't want to duplicate or change the order of execution) diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index 6984017781..c998d034b8 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -790,6 +790,78 @@ type Raw = let describe (s: Shape) = match s with Circle _ -> "circle" | Rect _ -> "rect" let kind (id: Id) = match id with Name _ -> "name" | Slug _ -> "slug" let raw (r: Raw) = match r with Obj _ -> "obj" | Text _ -> "text" +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with an interface field compiles when the interface declares the tag" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + +[] +type Command = + | Take of Take + | Save + +let fromIndex (c: Command) = match c with Take t -> t.fromIndex | Save -> 0 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with an interface field reports an interface without the tag member" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract fromIndex: int + +[] +type Command = + | Take of Take + | Save +""" + compile source + |> Assert.Exists.errorWith "Case 'Take' of 'Command' compiles to its 'Take' value, so the interface must declare a 'type' member" + |> ignore + + testCase "TS tagged union case with a named interface field does not need the tag member" <| fun _ -> + let source = + """ +open Fable.Core + +type Take = + abstract fromIndex: int + +[] +type Command = + | Take of take: Take + | Save + +let fromIndex (c: Command) = match c with Take t -> t.fromIndex | Save -> 0 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TS tagged union case with a single primitive field compiles" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type AuthState = + | Zero + | One of string + +let one = AuthState.One "a" """ compile source |> Assert.Is.strictSuccess diff --git a/tests/Integration/Integration/CompilationTests.fs b/tests/Integration/Integration/CompilationTests.fs index c96715bfa0..c6b67745cc 100644 --- a/tests/Integration/Integration/CompilationTests.fs +++ b/tests/Integration/Integration/CompilationTests.fs @@ -90,17 +90,27 @@ let tests = else [||] + let outputArgs = + if Directory.GetFileSystemEntries(testCaseDir, "*.ts.expected") |> Array.isEmpty then + [| "-e"; ".jsx.actual" |] + else + [| "--lang"; "ts"; "-e"; ".ts.actual" |] + // Compile project let exitCode = - Array.append - [| project; "--cwd"; $"'%s{testCaseDir}'"; "-e"; ".jsx.actual"; "--exclude"; "Fable.Core" |] - precompiledLibArgs + Array.concat + [ + [| project; "--cwd"; $"'%s{testCaseDir}'" |] + outputArgs + [| "--exclude"; "Fable.Core" |] + precompiledLibArgs + ] |> Fable.Cli.Entry.main Expect.equal exitCode 0 "Expected exit code to be 0" let normalize content = - Regex.Replace(content, @"(/fable-library-js)[^/]+", "$1") + Regex.Replace(content, @"(/fable-library-(?:js|ts))[^/]+", "$1") |> fun c -> Regex.Replace(c, @"[^""]*/precompiled/", "precompiled/") |> _.ReplaceLineEndings() |> _.Trim() diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs new file mode 100644 index 0000000000..f932a11e32 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.fs @@ -0,0 +1,25 @@ +module TaggedUnion + +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + +type Edit = + abstract text: string + +[] +type Command = + | Take of Take + | Edit of edit: Edit + | Save + +let make (t: Take) = Take t + +let describe (c: Command) = + match c with + | Take t -> t.fromIndex + t.toIndex + | Edit e -> e.text.Length + | Save -> 0 diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected new file mode 100644 index 0000000000..ebd309f60a --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/TaggedUnion.jsx.expected @@ -0,0 +1,19 @@ + + +export function make(t) { + return t; +} + +export function describe(c) { + switch (c.type) { + case "edit": + return c.edit.text.length | 0; + case "save": + return 0; + default: { + const t = c; + return (t.fromIndex + t.toIndex) | 0; + } + } +} + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj b/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj new file mode 100644 index 0000000000..151946452c --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterface/tsTaggedUnionInterface.fsproj @@ -0,0 +1,16 @@ + + + + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs new file mode 100644 index 0000000000..f932a11e32 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.fs @@ -0,0 +1,25 @@ +module TaggedUnion + +open Fable.Core + +type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + +type Edit = + abstract text: string + +[] +type Command = + | Take of Take + | Edit of edit: Edit + | Save + +let make (t: Take) = Take t + +let describe (c: Command) = + match c with + | Take t -> t.fromIndex + t.toIndex + | Edit e -> e.text.Length + | Save -> 0 diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected new file mode 100644 index 0000000000..30f02a1924 --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/TaggedUnion.ts.expected @@ -0,0 +1,35 @@ + +import { int32 } from "./fable_modules/fable-library-ts.5.23.0/Int32.ts"; + +export interface Take { + get fromIndex(): int32, + get toIndex(): int32, + get type(): string +} + +export interface Edit { + get text(): string +} + +export type Command = + | Take & { type: "take" } + | { type: "edit", edit: Edit } + | { type: "save" } + +export function make(t: Take): Command { + return t as Command; +} + +export function describe(c: Command): int32 { + switch (c.type) { + case "edit": + return c.edit.text.length | 0; + case "save": + return 0; + default: { + const t = c as Take; + return (t.fromIndex + t.toIndex) | 0; + } + } +} + diff --git a/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj new file mode 100644 index 0000000000..151946452c --- /dev/null +++ b/tests/Integration/Integration/data/tsTaggedUnionInterfaceTs/tsTaggedUnionInterfaceTs.fsproj @@ -0,0 +1,16 @@ + + + + net10.0 + Major + + + + + + + + + + + diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 4fdd65d4b5..978240b835 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -451,6 +451,48 @@ module TaggedUnion = | [] Off of flag: bool | [] Idle + type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + + type Edit = + abstract text: string + + type TakeRange(fromIndex: int, toIndex: int) = + let mutable toIndex = toIndex + member _.Extend(n: int) = toIndex <- toIndex + n + interface Take with + member _.``type`` = "take" + member _.fromIndex = fromIndex + member _.toIndex = toIndex + + [] + type Command = + | Take of Take + | Edit of edit: Edit + | Save + + [] + type AuthState = + | Zero + | One of string + | Two of string * string + + type Circle = + abstract ``type``: string + abstract radius: float + + type Rect = + abstract ``type``: string + abstract width: float + abstract height: float + + [] + type Shape = + | [] Circle of Circle + | [] Rect of Rect + module ErasedCaseTest = type Circle = abstract radius: float @@ -1445,6 +1487,58 @@ let tests = !!"n:raw" |> describeId |> equal "name n:raw" !!"raw" |> describeId |> equal "slug raw" !!7 |> describeId |> equal "count 7" + + testCase "TS tagged union case with an interface field compiles to the value itself" <| fun () -> + let range = TaggedUnion.TakeRange(1, 3) + let command = TaggedUnion.Command.Take range + obj.ReferenceEquals(command, range) |> equal true + jsTypeof command?Item |> equal "undefined" + command?``type`` |> equal "take" + command.IsTake |> equal true + command.IsSave |> equal false + range.Extend 2 + match command with + | TaggedUnion.Command.Take t -> t.toIndex |> equal 5 + | _ -> failwith "expected Take" + + testCase "TS tagged union case with an interface field reads raw JS objects" <| fun () -> + let describe = function + | TaggedUnion.Command.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.Command.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.Command.Save -> "save" + !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" + !!{| ``type`` = "edit"; edit = {| text = "hi" |} |} |> describe |> equal "edit: hi" + !!{| ``type`` = "save" |} |> describe |> equal "save" + + testCase "TS tagged union case with a named interface field compiles to an object" <| fun () -> + let edit = { new TaggedUnion.Edit with member _.text = "hi" } + let command = TaggedUnion.Command.Edit edit + obj.ReferenceEquals(command, edit) |> equal false + command?``type`` |> equal "edit" + obj.ReferenceEquals(command?edit, edit) |> equal true + match command with + | TaggedUnion.Command.Edit e -> e.text |> equal "hi" + | _ -> failwith "expected Edit" + + // See #3837 + testCase "TS tagged union case with a single primitive field carries the tag" <| fun () -> + let one = TaggedUnion.AuthState.One "a" + one?``type`` |> equal "one" + one?Item |> equal "a" + let describe = function + | TaggedUnion.AuthState.Zero -> "zero" + | TaggedUnion.AuthState.One s -> "one: " + s + | TaggedUnion.AuthState.Two(a, b) -> "two: " + a + b + one |> describe |> equal "one: a" + TaggedUnion.AuthState.Two("a", "b") |> describe |> equal "two: ab" + TaggedUnion.AuthState.Zero |> describe |> equal "zero" + + testCase "TS tagged union cases with interface fields sharing a tag are told apart by the interface members" <| fun () -> + let describe = function + | TaggedUnion.Shape.Circle c -> sprintf "circle: %.1f" c.radius + | TaggedUnion.Shape.Rect r -> sprintf "rect: %.1f x %.1f" r.width r.height + !!{| ``type`` = "shape"; radius = 2.0 |} |> describe |> equal "circle: 2.0" + !!{| ``type`` = "shape"; width = 2.0; height = 3.0 |} |> describe |> equal "rect: 2.0 x 3.0" #endif testCase "Pattern matching with StringEnum works" <| fun () -> diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index cdb0e1797b..fe45121f4f 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -26,6 +26,21 @@ module PojoInterface = member val name = name member val breed = breed member val age = age with get, set + +module TaggedUnion = + type Take = + abstract ``type``: string + abstract fromIndex: int + abstract toIndex: int + + type Edit = + abstract text: string + + [] + type Command = + | Take of Take + | Edit of edit: Edit + | Save #endif let tests = @@ -60,5 +75,22 @@ let tests = dog.age |> equal None dog.age <- Some 5 dog.age |> equal (Some 5) + + testCase "TS tagged union case with an interface field compiles to the value itself" <| fun () -> + let take = + { new TaggedUnion.Take with + member _.``type`` = "take" + member _.fromIndex = 1 + member _.toIndex = 3 } + let describe = function + | TaggedUnion.Command.Take t -> sprintf "take: %d-%d" t.fromIndex t.toIndex + | TaggedUnion.Command.Edit e -> sprintf "edit: %s" e.text + | TaggedUnion.Command.Save -> "save" + let command = TaggedUnion.Command.Take take + obj.ReferenceEquals(command, take) |> equal true + command |> describe |> equal "take: 1-3" + TaggedUnion.Command.Edit { new TaggedUnion.Edit with member _.text = "hi" } |> describe |> equal "edit: hi" + TaggedUnion.Command.Save |> describe |> equal "save" + !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" #endif ] From 2098f64a25e96385306ef19ed8c289f3b6c84db9 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 22 Oct 2026 22:13:22 +0200 Subject: [PATCH 226/322] feat(all): warn on duplicate CompiledName on union cases --- src/Fable.Transforms/FSharp2Fable.fs | 25 ++++++++ src/Fable.Transforms/Global/WarningCodes.fs | 5 ++ .../Compiler/BehaviorWarningTests.fs | 63 +++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index b75db8460c..0a16122a88 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2337,6 +2337,30 @@ let private checkInlinedTaggedUnionCases (com: FableCompiler) ctx (ent: FSharpEn |> addError (com :> Compiler) ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) | _ -> () +let private warnDuplicateCompiledNames (com: FableCompiler) ctx (ent: FSharpEntity) = + let compiledName (uci: FSharpUnionCase) = + match getUnionPattern (ent.AsType()) uci with + | StringEnum(_, rule) -> + match transformStringEnum rule uci with + | Fable.Value(Fable.StringConstant name, _) -> Some name + | _ -> None + | DiscriminatedUnion _ -> FsUnionCase.CompiledName uci |> Option.defaultValue uci.Name |> Some + // TypeScriptTaggedUnion cases sharing a tag are supported and checked separately + | _ -> None + + // Beam reports collisions on the Erlang atom itself, see `reportUnionCaseTagCollisions` + if ent.IsFSharpUnion && (com :> Compiler).Options.Language <> Beam then + ent.UnionCases + |> Seq.choose (fun uci -> compiledName uci |> Option.map (fun name -> name, uci)) + |> Seq.groupBy fst + |> Seq.iter (fun (name, cases) -> + let _, first = Seq.head cases + + for _, uci in Seq.tail cases do + WarningCodes.duplicateCompiledName ent.DisplayName uci.Name first.Name name + |> addWarningWithCode com ctx.InlinePath (makeRange uci.DeclarationLocation |> Some) + ) + let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = fsDecls |> List.collect (fun fsDecl -> @@ -2344,6 +2368,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = | FSharpImplementationFileDeclaration.Entity(fsEnt, sub) -> warnIgnoredCaseTests com ctx fsEnt checkInlinedTaggedUnionCases com ctx fsEnt + warnDuplicateCompiledNames com ctx fsEnt match sub with | [] when isIgnoredLeafEntity fsEnt -> [] diff --git a/src/Fable.Transforms/Global/WarningCodes.fs b/src/Fable.Transforms/Global/WarningCodes.fs index b5b34e627d..c2d67bb1b5 100644 --- a/src/Fable.Transforms/Global/WarningCodes.fs +++ b/src/Fable.Transforms/Global/WarningCodes.fs @@ -130,6 +130,10 @@ let staticDoBindingsDisabled = let regexFlagUnsupported (flag: string) = "FABLE0120", $"Regex %s{flag} flag is not supported in Php" +/// Two cases of a union compile to the same name, so the output cannot tell them apart. +let duplicateCompiledName (unionName: string) (caseName: string) (otherCaseName: string) (compiledName: string) = + "FABLE0121", $"Cases '%s{caseName}' and '%s{otherCaseName}' of '%s{unionName}' both compile to '%s{compiledName}'" + /// Every code the compiler can emit, both bands. A directive naming anything else is reported as /// a typo, so a new warning MUST be added to this list as well as defined above. let knownCodes = @@ -159,6 +163,7 @@ let knownCodes = charSurrogateTestsAreFalse staticDoBindingsDisabled regexFlagUnsupported "" + duplicateCompiledName "" "" "" "" ] |> List.map fst |> Set.ofList diff --git a/tests/Integration/Compiler/BehaviorWarningTests.fs b/tests/Integration/Compiler/BehaviorWarningTests.fs index c38f57e10f..938dab7b97 100644 --- a/tests/Integration/Compiler/BehaviorWarningTests.fs +++ b/tests/Integration/Compiler/BehaviorWarningTests.fs @@ -113,4 +113,67 @@ let isName (id: Id) = match id with Name _ -> true | Slug _ -> false compile source |> Assert.Code.warning "FABLE0116" |> ignore + + testCase "Duplicate CompiledName on union cases is reported" <| fun _ -> + let source = + """ +type Prop = + | [] RefHook of int + | [] RefValue of string +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'RefValue' and 'RefHook' of 'Prop' both compile to 'ref'" + |> ignore + + testCase "CompiledName clashing with another case name is reported" <| fun _ -> + let source = + """ +type Id = + | [] Name + | Slug +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> ignore + + testCase "StringEnum values clashing through the case rule are reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Kind = + | [] S + | Small +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'Small' and 'S' of 'Kind' both compile to 'small'" + |> ignore + + testCase "Duplicate CompiledName can be suppressed by code" <| fun _ -> + let source = + """ +type Prop = + | [] RefHook of int + | [] RefValue of string // fable-disable-line FABLE0121 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "TypeScriptTaggedUnion cases sharing a tag are not reported as duplicates" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type Terminal = + | [] Done of value: float + | [] DoneError of error: string +""" + compile source + |> Assert.Is.strictSuccess + |> ignore ] From be20361ffaf44ccc91a430501caec6e65db31db2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:28:14 +0200 Subject: [PATCH 227/322] chore: release multiple projects (#5125) chore: fix prerelease dependency bumps in fable-compiler-js --- src/Fable.Cli/CHANGELOG.md | 40 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 36 +++++++++++++++++++++- src/Fable.Core/CHANGELOG.md | 13 +++++++- src/Fable.Transforms/Global/Compiler.fs | 4 +-- src/fable-compiler-js/CHANGELOG.md | 25 +++++++++++++++- src/fable-compiler-js/package.json | 4 +-- src/fable-library-ts/CHANGELOG.md | 21 ++++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-metadata/CHANGELOG.md | 2 +- src/fable-standalone/CHANGELOG.md | 32 ++++++++++++++++++-- src/fable-standalone/package.json | 4 +-- 11 files changed, 168 insertions(+), 15 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index ad8b736213..e6abb87713 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.24.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(all)* Kill --run process on SIGTERM everywhere (#5140) ([09158b05](https://github.com/fable-compiler/Fable/commit/09158b05acf4c35b54111f4f1d72b96930bce18e)) +* *(all)* Render warning code without repeating the tag (#5142) ([5c44e514](https://github.com/fable-compiler/Fable/commit/5c44e514626a230227e2aad040d439fa7da0491c)) +* *(all)* Serialize output directory deduplication (#5143) ([b775276e](https://github.com/fable-compiler/Fable/commit/b775276eb843c5bf6533424b325eef25940abcaa)) +* *(all)* Report compiler agent failures instead of hanging (#5144) ([09d06614](https://github.com/fable-compiler/Fable/commit/09d066146f662e331d1b73c0ba60f2a0ed935fbc)) +* *(all)* Refresh unchanged outputs so up-to-date skip works (#5145) ([0529fcf8](https://github.com/fable-compiler/Fable/commit/0529fcf8b57cc5a6aca0a92aae5f444e53ea0f28)) +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +### ⚡ Performance Improvements + +* *(all)* Faster project cracking (#5136) ([ba042783](https://github.com/fable-compiler/Fable/commit/ba0427839107c157436479958898688952df05c3)) +* *(all)* Disable .NET telemetry for dotnet build (#5137) ([e9ffc41e](https://github.com/fable-compiler/Fable/commit/e9ffc41e5f53df018d4c141b9bfaa51ca76e0be5)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 5.23.0 - 2026-10-21 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 7a62375a71..3ead4b55d5 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.23.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(all)* Kill --run process on SIGTERM everywhere (#5140) ([09158b05](https://github.com/fable-compiler/Fable/commit/09158b05acf4c35b54111f4f1d72b96930bce18e)) +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +### ⚡ Performance Improvements + +* *(all)* Faster project cracking (#5136) ([ba042783](https://github.com/fable-compiler/Fable/commit/ba0427839107c157436479958898688952df05c3)) +* *(all)* Disable .NET telemetry for dotnet build (#5137) ([e9ffc41e](https://github.com/fable-compiler/Fable/commit/e9ffc41e5f53df018d4c141b9bfaa51ca76e0be5)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 5.22.0 - 2026-10-21 ### 🚀 Features diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index e8d98f5165..96f35da731 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 --- # Changelog @@ -9,6 +9,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.6.0 - 2026-10-22 + +### 🚀 Features + +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 5.5.0 - 2026-10-21 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 0820c45006..b4e6f72d09 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.23.0" + let VERSION = "5.24.0" [] - let JS_LIBRARY_VERSION = "2.9.0" + let JS_LIBRARY_VERSION = "2.10.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index eb47a9e68c..f0f881060f 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.2.0 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 4.1.0 - 2026-10-21 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 31ac1b46c7..9f09e22bbf 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "4.1.0", + "version": "4.2.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.2.0", - "@fable-org/fable-standalone": "^4.0.0-beta.2-beta.1" + "@fable-org/fable-standalone": "^4.0.0-beta.3" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 158e8745ae..81142079de 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: e33c57870454fea189a3eb33519f583a5003f8c3 +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 updaters: - package.json: file: package.json @@ -15,6 +15,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.10.0 - 2026-10-22 + +### 🚀 Features + +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/e33c57870454fea189a3eb33519f583a5003f8c3..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 2.9.0 - 2026-10-16 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index 78c9d2ca7b..8bcda262e0 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.9.0", + "version": "2.10.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-metadata/CHANGELOG.md b/src/fable-metadata/CHANGELOG.md index 4681dfd1d4..3736b6bab7 100644 --- a/src/fable-metadata/CHANGELOG.md +++ b/src/fable-metadata/CHANGELOG.md @@ -5,7 +5,7 @@ updaters: file: package.json - regex: file: ../fable-compiler-js/package.json - pattern: '(?<="@fable-org/fable-metadata": "\^)\d+\.\d+\.\d+' + pattern: '(?<="@fable-org/fable-metadata": "\^)[^"]+' --- # Changelog diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 78ef6561de..4f936ea804 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 5ee8bbb832d9677ad702104ded8e00c09619393f +last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 pre_release: beta include: - ../Fable.Transforms/ @@ -11,7 +11,7 @@ updaters: file: package.json - regex: file: ../fable-compiler-js/package.json - pattern: '(?<="@fable-org/fable-standalone": "\^)\d+\.\d+\.\d+' + pattern: '(?<="@fable-org/fable-standalone": "\^)[^"]+' --- # Changelog @@ -21,6 +21,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.3 - 2026-10-22 + +### 🚀 Features + +* *(all)* Support suppressing warnings via `// fable-disable` comments (#4808) ([f4a1a8cd](https://github.com/fable-compiler/Fable/commit/f4a1a8cdae2e8ac81edbdc6eba36cc140234170b)) +* *(all)* Warn on duplicate CompiledName on union cases ([2098f64a](https://github.com/fable-compiler/Fable/commit/2098f64a25e96385306ef19ed8c289f3b6c84db9)) +* *(js/ts)* Map static Decimal conversion methods (#5129) ([cd63e57e](https://github.com/fable-compiler/Fable/commit/cd63e57e909393ab493a1d8bfedbea9a50a377c9)) +* *(js/ts)* Support System.Diagnostics.Stopwatch (#5100) ([eb68225f](https://github.com/fable-compiler/Fable/commit/eb68225ff74d1b455765d99998faa6fd3be32383)) +* *(js/ts)* Add `JSX.spread` for `JSX.create` props (#5141) ([28427d62](https://github.com/fable-compiler/Fable/commit/28427d621b4e256fdcda83dd8340ed6817e0e60c)) +* *(js/ts)* Derive case tests for TypeScriptTaggedUnion cases sharing a tag (#5130) ([3b8a9e21](https://github.com/fable-compiler/Fable/commit/3b8a9e218d27a47074b894f4768466f74c5a0f8e)) +* *(js/ts)* Support CaseTest on Erase union cases (#5131) ([8cc21077](https://github.com/fable-compiler/Fable/commit/8cc210773e7b30bcf1071388d603b5c115cccd85)) +* *(js/ts)* Derive erased union tests from interfaces (#5133) ([3a295308](https://github.com/fable-compiler/Fable/commit/3a2953087fd6749f211263b049d94eafd289472d)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round decimals below 0.1 correctly ([fca4c408](https://github.com/fable-compiler/Fable/commit/fca4c408a64e33c58293f12869975e5b61bcb96a)) +* *(js/ts)* Keep decimal zero scale without leading zeros ([3076b45a](https://github.com/fable-compiler/Fable/commit/3076b45acacf39b8c99ec57025560ad355ab74db)) +* *(js/ts)* Hash decimals by value in collections ([32a69e0d](https://github.com/fable-compiler/Fable/commit/32a69e0d00f567c9a838307e9ed51b3b1d519473)) +* *(js/ts)* Keep .NET decimal scale through arithmetic (#5126) ([bdf0a6c8](https://github.com/fable-compiler/Fable/commit/bdf0a6c8cb56151a5431b5e3703f7ee165b26dba)) +* *(js/ts)* Warn on type test against Pojo class (#5128) ([798df101](https://github.com/fable-compiler/Fable/commit/798df10179576a28f602fad58e204881792a0aef)) +* *(js/ts)* Accept decimal integers up to 2^96 ([c9feb339](https://github.com/fable-compiler/Fable/commit/c9feb339fb8a4e98bebf17b3facdc42ecd22d628)) +* *(js/ts)* Align decimal conversion, parsing and G format ([6430cdac](https://github.com/fable-compiler/Fable/commit/6430cdacec34e6327434af649eb0c9427d65608c)) +* *(js/ts)* Add missing Async and Task members, cancel timed-out StartChild, bind Async in task { } (#5097) ([d063630d](https://github.com/fable-compiler/Fable/commit/d063630d58d62db21603d45a6bf54b172378c66e)) +* *(js/ts)* Respect maxDegreeOfParallelism and cancel on failure in Async.Parallel (#5098) ([5de3271d](https://github.com/fable-compiler/Fable/commit/5de3271d8b47f1752c4e74c010e4314cf40bbcbe)) +* *(js/ts)* Inline interface cases of TypeScriptTaggedUnion (#5146) ([2be66611](https://github.com/fable-compiler/Fable/commit/2be66611d884cd564fa3f7b06fd4a015add1cefd)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/5ee8bbb832d9677ad702104ded8e00c09619393f..2098f64a25e96385306ef19ed8c289f3b6c84db9) + ## 4.0.0-beta.2 - 2026-10-21 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index bfb4678383..93dc09c189 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "4.0.0-beta.2", - "fableVersion": "5.23.0", + "version": "4.0.0-beta.3", + "fableVersion": "5.24.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 459166d2500a963a3616d8b1a63e1d8585d61ec6 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 15:47:34 +0100 Subject: [PATCH 228/322] fix(all): warn on duplicate names in erased unions (#5169) --- src/Fable.Transforms/FSharp2Fable.fs | 7 +++-- .../Compiler/BehaviorWarningTests.fs | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 0a16122a88..5e577af0c9 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2339,11 +2339,14 @@ let private checkInlinedTaggedUnionCases (com: FableCompiler) ctx (ent: FSharpEn let private warnDuplicateCompiledNames (com: FableCompiler) ctx (ent: FSharpEntity) = let compiledName (uci: FSharpUnionCase) = - match getUnionPattern (ent.AsType()) uci with - | StringEnum(_, rule) -> + let stringEnumName rule = match transformStringEnum rule uci with | Fable.Value(Fable.StringConstant name, _) -> Some name | _ -> None + + match getUnionPattern (ent.AsType()) uci with + | StringEnum(_, rule) -> stringEnumName rule + | ErasedUnion(_, _, rule) when uci.Fields.Count = 0 -> stringEnumName rule | DiscriminatedUnion _ -> FsUnionCase.CompiledName uci |> Option.defaultValue uci.Name |> Some // TypeScriptTaggedUnion cases sharing a tag are supported and checked separately | _ -> None diff --git a/tests/Integration/Compiler/BehaviorWarningTests.fs b/tests/Integration/Compiler/BehaviorWarningTests.fs index 938dab7b97..dd60f88717 100644 --- a/tests/Integration/Compiler/BehaviorWarningTests.fs +++ b/tests/Integration/Compiler/BehaviorWarningTests.fs @@ -137,6 +137,35 @@ type Id = |> Assert.Code.warning "FABLE0121" |> ignore + testCase "Duplicate CompiledName on field-less erased union cases is reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type X = + | [] A + | [] B +""" + compile source + |> Assert.Code.warning "FABLE0121" + |> Assert.Exists.warningWith "Cases 'B' and 'A' of 'X' both compile to 'ff'" + |> ignore + + testCase "Duplicate CompiledName on erased union cases with a field is not reported" <| fun _ -> + let source = + """ +open Fable.Core + +[] +type X = + | [] A of int + | [] B of string +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + testCase "StringEnum values clashing through the case rule are reported" <| fun _ -> let source = """ From 44495104b5c7ac65bfcea184d1305fd29f1db9b4 Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:46:43 +0800 Subject: [PATCH 229/322] perf(js/ts): standard library combine numeric hash from snapshot of object instead of mapping (#5151) Co-authored-by: Mangel Maxime --- src/fable-library-ts/Util.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 00ad548e9a..bd5938653f 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -400,12 +400,15 @@ export function bigintHash(x: bigint) { } // From https://stackoverflow.com/a/37449594 +function combineHashCode(h1: number, h2: number): number { + return ((h1 << 5) + h1) ^ h2; +} + export function combineHashCodes(hashes: ArrayLike) { let h1 = 0; const len = hashes.length; for (let i = 0; i < len; i++) { - const h2 = hashes[i]; - h1 = ((h1 << 5) + h1) ^ h2; + h1 = combineHashCode(h1, hashes[i]); } return h1; } @@ -471,8 +474,13 @@ export function structuralHash(x: T): number { return dateHash(x); } else if (Object.getPrototypeOf(x)?.constructor === Object) { // TODO: check call-stack to prevent cyclic objects? - const hashes = Object.values(x).map((v) => structuralHash(v)); - return combineHashCodes(hashes); + // Snapshot values before user hashing can mutate the object. + const values = Object.values(x); + let hash = 0; + for (let i = 0; i < values.length; i++) { + hash = combineHashCode(hash, structuralHash(values[i])); + } + return hash; } else { // Classes don't implement GetHashCode by default, but must use identity hashing return numberHash(ObjectRef.id(x)); From 60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Fri, 23 Oct 2026 10:41:22 -0500 Subject: [PATCH 230/322] fix(js/ts/python/dart/beam): raise typed exceptions from invalidArg and invalidOp `invalidArg` and `invalidOp` threw a plain `Exception`, so `:? ArgumentException` and `:? InvalidOperationException` handlers never caught them, nor the library errors raised through them. They now go through `FSharp.Core.Operators` like `nullArg`, raising the `System` exceptions the libraries already define. Beam tags its exception maps inline, as it does for those exceptions' constructors. --- src/Fable.Transforms/Beam/Replacements.fs | 9 ++++++--- src/Fable.Transforms/Dart/Replacements.fs | 8 ++------ src/Fable.Transforms/Python/Replacements.fs | 9 +++------ src/Fable.Transforms/Replacements.fs | 8 ++------ src/fable-library-dart/FSharp.Core.fs | 8 ++++++++ src/fable-library-ts/FSharp.Core.fs | 8 ++++++++ tests/Beam/ExceptionTests.fs | 20 ++++++++++++++++++++ tests/Dart/src/MiscTests.fs | 18 ++++++++++++++++++ tests/Js/Main/MiscTests.fs | 18 ++++++++++++++++++ tests/Python/TestMisc.fs | 20 ++++++++++++++++++++ 10 files changed, 105 insertions(+), 21 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index f590ef4455..2253293e98 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -329,13 +329,16 @@ let private operators | ("DefaultArg" | "DefaultValueArg"), [ opt; defValue ] -> Helper.LibCall(com, "fable_option", "default_value", _t, [ opt; defValue ]) |> Some - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r _t msg |> Some + | "FailWith", [ msg ] -> makeThrow r _t msg |> Some + | "InvalidOp", [ msg ] -> + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => invalid_operation_exception, message => $0}") + |> Some | "InvalidArg", [ argName; msg ] -> let msg = add msg (add (add (Value(StringConstant " (Parameter '", None)) argName) (Value(StringConstant "')", None))) - makeThrow r _t msg |> Some + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_exception, message => $0}") + |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> let msg = diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 86ea19665a..65a3c7311e 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1036,16 +1036,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> Extended(Throw(None, t), r) |> Some // Math functions diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index e404a973fb..248e3266ef 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1216,18 +1216,15 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // bprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "NullArg" | "Using"), _ -> + fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args | "Lock", _ -> Helper.LibCall(com, "util", "lock", t, args, i.SignatureArgTypes, ?loc = r) |> Some // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 2af2d5e382..8109a1ae2a 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1562,16 +1562,12 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions - | "FailWith", [ msg ] - | "InvalidOp", [ msg ] -> makeThrow r t (error com msg) |> Some - | "InvalidArg", [ argName; msg ] -> - let msg = add msg (add (add (str " (Parameter '") argName) (str "')")) - makeThrow r t (error com msg) |> Some + | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some | "Raise", [ arg ] -> makeThrow r t arg |> Some | "Reraise", _ -> match ctx.CaughtException with diff --git a/src/fable-library-dart/FSharp.Core.fs b/src/fable-library-dart/FSharp.Core.fs index 71ae90e717..89cdb1338a 100644 --- a/src/fable-library-dart/FSharp.Core.fs +++ b/src/fable-library-dart/FSharp.Core.fs @@ -14,6 +14,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/src/fable-library-ts/FSharp.Core.fs b/src/fable-library-ts/FSharp.Core.fs index 4257e6aed2..42a163060c 100644 --- a/src/fable-library-ts/FSharp.Core.fs +++ b/src/fable-library-ts/FSharp.Core.fs @@ -41,6 +41,14 @@ module Operators = let nullArg (argumentName: string) = raise (System.ArgumentNullException(argumentName)) + [] + let invalidArg (argumentName: string) (message: string) = + raise (System.ArgumentException(message, argumentName)) + + [] + let invalidOp (message: string) = + raise (System.InvalidOperationException(message)) + [] let using<'T, 'R when 'T :> System.IDisposable and 'T: null> (resource: 'T) (action: 'T -> 'R) = try diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index 49413902e5..efd000fa8c 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -170,3 +170,23 @@ let ``test invalidArg formats the message like .NET`` () = throwsError "This is invalid (Parameter 'arg')" (fun () -> invalidArg "arg" "This is invalid" ) + +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index c6278ff2ec..fb0c2a645e 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -155,6 +155,24 @@ let tests() = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + testCase "Compiler target flags have correct value per target" <| fun () -> equal false Compiler.isJavaScript equal false Compiler.isTypeScript diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 5b3e359150..910b403556 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -1260,6 +1260,24 @@ let tests = invalidArg "arg" "This is invalid" ) + testCase "invalidArg raises ArgumentException" <| fun () -> + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid (Parameter 'arg')" caught + + testCase "invalidOp raises InvalidOperationException" <| fun () -> + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + equal "This is invalid" caught + testCase "Pattern matching optimization works (switch statement)" <| fun () -> let mutable x = "" let i = 4 diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 44eca8a4cc..0fe681a1f1 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1277,6 +1277,26 @@ let ``test invalidArg formats the message like .NET`` () = invalidArg "arg" "This is invalid" ) +[] +let ``test invalidArg raises ArgumentException`` () = + let caught = + try + invalidArg "arg" "This is invalid" + with + | :? System.ArgumentException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid (Parameter 'arg')" + +[] +let ``test invalidOp raises InvalidOperationException`` () = + let caught = + try + invalidOp "This is invalid" + with + | :? System.InvalidOperationException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "This is invalid" + # nowarn "26" // This rule will never be matched (code 26) [] From 0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 17:38:41 +0100 Subject: [PATCH 231/322] fix(beam): raise ArgumentNullException from nullArg --- src/Fable.Transforms/Beam/Replacements.fs | 3 ++- tests/Beam/ExceptionTests.fs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 2253293e98..38f890947c 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -346,7 +346,8 @@ let private operators (Value(StringConstant "Value cannot be null. (Parameter '", None)) (add arg (Value(StringConstant "')", None))) - makeThrow r _t msg |> Some + makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_null_exception, message => $0}") + |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some // Nullable active patterns and helpers diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index efd000fa8c..d6e9119c2b 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -190,3 +190,13 @@ let ``test invalidOp raises InvalidOperationException`` () = | :? System.InvalidOperationException as e -> e.Message | e -> "Unexpected exception: " + e.Message caught |> equal "This is invalid" + +[] +let ``test nullArg raises ArgumentNullException`` () = + let caught = + try + nullArg "arg" + with + | :? System.ArgumentNullException as e -> e.Message + | e -> "Unexpected exception: " + e.Message + caught |> equal "Value cannot be null. (Parameter 'arg')" From 9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 26 Oct 2026 18:54:04 +0100 Subject: [PATCH 232/322] fix(beam): keep parameter name in ArgumentException --- src/Fable.Transforms/Beam/Replacements.fs | 52 +++++++++++++++-------- src/fable-library-beam/fable_utils.erl | 9 ++++ tests/Beam/MiscTests.fs | 32 ++++++++++++++ 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 38f890947c..4b3df7f340 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -6227,25 +6227,43 @@ let tryCall let typeTag = typeName.Substring(typeName.LastIndexOf('.') + 1) |> sanitizeErlangName + let make msg inner = + match inner with + | None -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" + | Some inner -> + emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" + + let withParamName msg paramName = + Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) + + let isArgumentException = typeName = "System.ArgumentException" + + // ArgumentNullException and ArgumentOutOfRangeException take (paramName, message), the others (message, paramName) + let paramNameFirst = + typeName = "System.ArgumentNullException" + || typeName = "System.ArgumentOutOfRangeException" + + let defaultMessage = + match typeName with + | "System.ArgumentException" -> "Value does not fall within the expected range." + | "System.ArgumentNullException" -> "Value cannot be null." + | "System.ArgumentOutOfRangeException" -> "Specified argument was out of the range of valid values." + | _ -> $"Exception of type '%s{typeName}' was thrown." + match info.CompiledName, thisArg, args with - | ".ctor", None, [ msg ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some - | ".ctor", None, [ msg; second ] -> + | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some + | ".ctor", None, [ paramName ] when paramNameFirst -> + make (withParamName (makeStrConst defaultMessage) paramName) None |> Some + | ".ctor", None, [ msg ] -> make msg None |> Some + | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - // (message, paramName): paramName is not modelled, only the message is kept - | [ _; String ] -> emitExpr r t [ msg ] $"#{{exn_type => %s{typeTag}, message => $0}}" |> Some - // (message, innerException) - | _ -> - emitExpr r t [ msg; second ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - |> Some - // (message, paramName, innerException) - | ".ctor", None, [ msg; _paramName; inner ] -> - emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - |> Some - | ".ctor", None, [] -> - let msg = $"Exception of type '%s{typeName}' was thrown." - - emitExpr r t [] $"#{{exn_type => %s{typeTag}, message => <<\"%s{msg}\">>}}" - |> Some + | [ _; String ] when paramNameFirst -> make (withParamName second first) None |> Some + | [ _; String ] when isArgumentException -> make (withParamName first second) None |> Some + | [ _; String ] -> make first None |> Some + | _ -> make first (Some second) |> Some + | ".ctor", None, [ msg; paramName; inner ] when isArgumentException -> + make (withParamName msg paramName) (Some inner) |> Some + | ".ctor", None, [ msg; _; inner ] -> make msg (Some inner) |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some | _ -> None diff --git a/src/fable-library-beam/fable_utils.erl b/src/fable-library-beam/fable_utils.erl index bcca5e5d90..6d5c72ac1b 100644 --- a/src/fable-library-beam/fable_utils.erl +++ b/src/fable-library-beam/fable_utils.erl @@ -25,6 +25,7 @@ to_list/1, enumerate_to_list/1, aggregate_exception/2, + argument_exception_message/2, exception_message/1, is_exception_type/2, new_byte_array/1, @@ -67,6 +68,7 @@ -spec to_list(term()) -> list(). -spec enumerate_to_list(term()) -> list(). -spec aggregate_exception(binary(), term()) -> map(). +-spec argument_exception_message(binary(), binary() | undefined) -> binary(). -spec exception_message(term()) -> binary(). -spec is_exception_type(term(), atom()) -> boolean(). -spec new_byte_array(list() | binary() | tuple()) -> tuple(). @@ -477,6 +479,13 @@ aggregate_exception(BaseMessage, ExceptionsValue) -> inner_exceptions => new_ref(Exceptions) }. +argument_exception_message(Message, undefined) -> + Message; +argument_exception_message(Message, <<>>) -> + Message; +argument_exception_message(Message, ParamName) -> + <>. + %% Built-in .NET exceptions carry a compact nominal tag. Follow the .NET exception %% hierarchy here so a catch for a base exception also accepts its derived exceptions. is_exception_type(Exception, Expected) when is_reference(Exception) -> diff --git a/tests/Beam/MiscTests.fs b/tests/Beam/MiscTests.fs index 2b14e17b7b..764d23b0e0 100644 --- a/tests/Beam/MiscTests.fs +++ b/tests/Beam/MiscTests.fs @@ -768,6 +768,38 @@ let ``test ArgumentException with message and inner exception works`` () = ex.Message |> equal "outer message" ex.InnerException.Message |> equal "the inner cause" +[] +let ``test ArgumentException default message matches .NET`` () = + let ex = System.ArgumentException() + ex.Message |> equal "Value does not fall within the expected range." + +[] +let ``test ArgumentException with message and parameter name works`` () = + let ex = System.ArgumentException("outer message", "arg") + ex.Message |> equal "outer message (Parameter 'arg')" + +[] +let ``test ArgumentException with message, parameter name and inner exception works`` () = + let inner = exn "the inner cause" + let ex = System.ArgumentException("outer message", "arg", inner) + ex.Message |> equal "outer message (Parameter 'arg')" + ex.InnerException.Message |> equal "the inner cause" + +[] +let ``test ArgumentNullException with parameter name works`` () = + let ex = System.ArgumentNullException("arg") + ex.Message |> equal "Value cannot be null. (Parameter 'arg')" + +[] +let ``test ArgumentNullException with parameter name and message works`` () = + let ex = System.ArgumentNullException("arg", "custom message") + ex.Message |> equal "custom message (Parameter 'arg')" + +[] +let ``test ArgumentOutOfRangeException with parameter name works`` () = + let ex = System.ArgumentOutOfRangeException("arg") + ex.Message |> equal "Specified argument was out of the range of valid values. (Parameter 'arg')" + [] let ``test Exception InnerException is null when not provided`` () = let ex = System.ArgumentException("no inner") From 0ceb5e9ec5b470d00295a5cea9ead1c621c7858e Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:56:47 +0800 Subject: [PATCH 233/322] perf(js/ts): standard library remove .map result array from record hash using snapshot (#5150) Co-authored-by: Mangel Maxime --- src/fable-library-ts/Types.ts | 11 ++++++++--- src/fable-library-ts/Util.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/fable-library-ts/Types.ts b/src/fable-library-ts/Types.ts index 2707bf65b8..dee38b1030 100644 --- a/src/fable-library-ts/Types.ts +++ b/src/fable-library-ts/Types.ts @@ -1,4 +1,4 @@ -import { Exception, IComparable, IEquatable, IHashable, combineHashCodes, compare, compareArrays, equalArrays, equals, sameConstructor, numberHash, structuralHash } from "./Util.ts"; +import { Exception, IComparable, IEquatable, IHashable, combineHashCode, combineHashCodes, compare, compareArrays, equalArrays, equals, sameConstructor, numberHash, structuralHash } from "./Util.ts"; // This type is only used internally for .ts files in the library // F# Result type is in Choice.fs @@ -123,8 +123,13 @@ function recordToString(self: T) { } function recordGetHashCode(self: T) { - const hashes = Object.values(self as any).map((v) => structuralHash(v)); - return combineHashCodes(hashes); + // Snapshot all values before calling any user-defined hash functions. + const values = Object.values(self as any); + let hash = 0; + for (let i = 0; i < values.length; i++) { + hash = combineHashCode(hash, structuralHash(values[i])); + } + return hash; } function recordEquals(self: T, other: T) { diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index bd5938653f..8276898e93 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -400,7 +400,7 @@ export function bigintHash(x: bigint) { } // From https://stackoverflow.com/a/37449594 -function combineHashCode(h1: number, h2: number): number { +export function combineHashCode(h1: number, h2: number): number { return ((h1 << 5) + h1) ^ h2; } From 7d7ceb33afd85a25bf06dc703e9d4a471cbc3292 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Mon, 26 Oct 2026 15:16:42 -0500 Subject: [PATCH 234/322] fix(js/ts/python/dart/beam): align collection exception types with FSharp.Core (#5153) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Dart/Replacements.fs | 26 +++++--- src/Fable.Transforms/Python/Replacements.fs | 15 +++-- src/Fable.Transforms/Replacements.fs | 10 ++- src/fable-library-beam/Range.fs | 2 +- src/fable-library-beam/Seq.fs | 24 +++++-- src/fable-library-beam/fable_list.erl | 60 +++++++++++++----- src/fable-library-beam/fable_map.erl | 10 ++- src/fable-library-dart/Array.fs | 63 +++++++++++-------- src/fable-library-dart/Global.fs | 2 +- src/fable-library-dart/List.fs | 29 ++++++--- src/fable-library-dart/Map.fs | 4 +- src/fable-library-dart/Range.fs | 2 +- src/fable-library-dart/Seq.fs | 24 +++++-- src/fable-library-dart/Set.fs | 8 +-- src/fable-library-py/fable_library/List.fs | 26 +++++--- src/fable-library-py/fable_library/Map.fs | 12 ++-- src/fable-library-py/fable_library/Range.fs | 2 +- src/fable-library-py/fable_library/Seq.fs | 22 +++++-- src/fable-library-py/fable_library/Set.fs | 10 +-- .../fable_library/seq_native.py | 9 ++- src/fable-library-ts/Array.fs | 59 +++++++++-------- src/fable-library-ts/Global.fs | 2 +- src/fable-library-ts/List.fs | 26 +++++--- src/fable-library-ts/Map.fs | 18 +++--- src/fable-library-ts/Range.fs | 2 +- src/fable-library-ts/Seq.fs | 24 +++++-- src/fable-library-ts/Set.fs | 10 +-- tests/Beam/ArrayTests.fs | 29 +++++++++ tests/Beam/ListTests.fs | 34 ++++++++++ tests/Beam/MapTests.fs | 6 ++ tests/Beam/SeqTests.fs | 34 ++++++++++ tests/Beam/Util.fs | 12 ++++ tests/Dart/src/ArrayTests.fs | 55 ++++++++++++++++ tests/Dart/src/ListTests.fs | 43 +++++++++++++ tests/Dart/src/MapTests.fs | 8 +++ tests/Dart/src/SeqTests.fs | 33 ++++++++++ tests/Dart/src/SetTests.fs | 18 ++++++ tests/Dart/src/Tests.Util.fs | 12 ++++ tests/Js/Main/ArrayTests.fs | 54 ++++++++++++++++ tests/Js/Main/ListTests.fs | 37 +++++++++++ tests/Js/Main/MapTests.fs | 16 +++++ tests/Js/Main/SeqTests.fs | 28 +++++++++ tests/Js/Main/SetTests.fs | 16 +++++ tests/Js/Main/Util/Util.fs | 13 ++++ tests/Python/TestList.fs | 40 ++++++++++++ tests/Python/TestMap.fs | 8 +++ tests/Python/TestSeq.fs | 39 ++++++++++++ tests/Python/TestSet.fs | 20 ++++++ tests/Python/Util.fs | 11 ++++ 49 files changed, 891 insertions(+), 176 deletions(-) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 65a3c7311e..77bed3bf65 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1874,14 +1874,24 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall( + com, + "List", + mangledName, + t, + [], + i.SignatureArgTypes, + genArgs = i.GenericArgs, + thisArg = x, + ?loc = r + ) + |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 248e3266ef..559fe0176a 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2107,14 +2107,13 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists - | ReplaceName [ "get_Head", "head" - "get_Tail", "tail" - "get_Item", "item" - "get_Length", "length" - "GetSlice", "getSlice" ] methName, - Some x, - _ -> + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + | ("get_Head" | "get_Tail"), Some x, _ -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "list", mangledName, t, [], thisArg = x, ?loc = r) |> Some + | ReplaceName [ "get_Item", "item"; "get_Length", "length"; "GetSlice", "getSlice" ] methName, Some x, _ -> let args = match args with | [ ExprType Unit ] -> [ x ] diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 8109a1ae2a..b96b3ef2f1 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2283,8 +2283,6 @@ let nativeArrayFunctions = "Filter", "filter" "ForAll", "every" "Iterate", "forEach" - "Reduce", "reduce" - "ReduceBack", "reduceRight" "SortInPlaceWith", "sort" |] @@ -2403,7 +2401,13 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let lists (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with - // Use methods for Head and Tail (instead of Get(ListHead) for example) to check for empty lists + // list.Head and list.Tail raise InvalidOperationException, List.head and List.tail ArgumentException + // PHP's FSharpList has no such members, so PHP keeps the head/tail functions below. + | ("get_Head" | "get_Tail"), Some x, _ when com.Options.Language <> Php -> + let mangledName = + Naming.buildNameWithoutSanitationFrom "FSharpList" false i.CompiledName "" + + Helper.LibCall(com, "List", mangledName, t, [], thisArg = x, ?loc = r) |> Some | ReplaceName [ "get_Head", "head" "get_Tail", "tail" "get_Item", "item" diff --git a/src/fable-library-beam/Range.fs b/src/fable-library-beam/Range.fs index e97846a5af..6d729e8cf7 100644 --- a/src/fable-library-beam/Range.fs +++ b/src/fable-library-beam/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs index bdb4b24110..71671d5394 100644 --- a/src/fable-library-beam/Seq.fs +++ b/src/fable-library-beam/Seq.fs @@ -992,7 +992,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -1000,7 +1000,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -1035,7 +1035,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1057,7 +1057,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take count (xs: seq<'T>) = generateIndexed @@ -1067,7 +1081,7 @@ let take count (xs: seq<'T>) = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 15c526e5f8..580eb7c9e5 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -225,10 +225,16 @@ sort_by_descending(Fn, Comparer, List) -> sort_with(Fn, List) -> lists:sort(fun(A, B) -> (Fn(A))(B) =< 0 end, List). +index_not_found() -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => <<"An index satisfying the predicate was not found in the collection.">> + }). + find(Fn, List) -> case lists:dropwhile(fun(E) -> not Fn(E) end, List) of [H | _] -> H; - [] -> erlang:error(<<"key_not_found">>) + [] -> index_not_found() end. try_find(Fn, List) -> @@ -245,7 +251,7 @@ find_index(Fn, [H | T], I) -> false -> find_index(Fn, T, I + 1) end; find_index(_Fn, [], _I) -> - erlang:error(<<"key_not_found">>). + index_not_found(). try_find_index(Fn, List) -> try_find_index(Fn, List, 0). @@ -387,7 +393,8 @@ try_item(_, _) -> undefined. exactly_one([X]) -> X; -exactly_one(_) -> erlang:error(<<"Sequence contains more than one element">>). +exactly_one([]) -> invalid_arg(<<"source">>, <<"The input sequence was empty.">>); +exactly_one(_) -> invalid_arg(<<"source">>, <<"The input sequence contains more than one element.">>). try_exactly_one([X]) -> fable_option:some(X); try_exactly_one(_) -> undefined. @@ -580,7 +587,7 @@ map_fold_back(Fn, List, State) -> pick(Fn, List) -> case try_pick(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); V -> V end. @@ -594,7 +601,7 @@ try_pick(Fn, [H | T]) -> find_index_back(Fn, List) -> case try_find_index_back(Fn, List) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> index_not_found(); Idx -> Idx end. @@ -673,7 +680,11 @@ get_slice(Lower, Upper, List) -> take(Count, List) -> case erlang:length(List) >= Count of true -> lists:sublist(List, Count); - false -> erlang:error(<<"The input sequence has an insufficient number of elements.">>) + false -> + erlang:error(#{ + exn_type => invalid_operation_exception, + message => <<"The input sequence has an insufficient number of elements.">> + }) end. %% ---- Random functions -------------------------------------------------------- @@ -697,13 +708,13 @@ random_shuffle(Xs) -> -spec random_choice_by(fun((ok) -> float()), list()) -> term(). random_choice_by(_Randomizer, []) -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); random_choice_by(Randomizer, Xs) -> Len = erlang:length(Xs), R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> lists:nth(erlang:trunc(R * Len) + 1, Xs) end. @@ -720,9 +731,9 @@ random_choice(Xs) -> random_choices_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); Count > 0 andalso Xs =:= [] -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); true -> Arr = erlang:list_to_tuple(Xs), Len = erlang:tuple_size(Arr), @@ -736,7 +747,7 @@ choices_loop(Randomizer, Arr, Len, N, Acc) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> E = erlang:element(erlang:trunc(R * Len) + 1, Arr), choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]) @@ -754,14 +765,14 @@ random_choices(Count, Xs) -> random_sample_by(Randomizer, Count, Xs) -> if Count < 0 -> - erlang:error(<<"The input must be non-negative.">>); + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); true -> Len = erlang:length(Xs), if Len =:= 0 andalso Count > 0 -> - erlang:error(<<"The input sequence was empty.">>); + invalid_arg(<<"source">>, <<"The input sequence was empty.">>); Count > Len -> - erlang:error(<<"The input sequence has an insufficient number of elements.">>); + invalid_arg(<<"count">>, <<"The input sequence has an insufficient number of elements.">>); true -> sample_loop(Randomizer, erlang:list_to_tuple(Xs), Len, Count, 0) end @@ -782,7 +793,7 @@ sample_loop(Randomizer, Arr, Len, Count, I) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> J = I + erlang:trunc(R * (Len - I)), EI = erlang:element(I + 1, Arr), @@ -798,7 +809,7 @@ shuffle_loop(Randomizer, Arr, I) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> - erlang:error(iolist_to_binary(io_lib:format("The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0).", [R]))); + randomizer_out_of_range(R); true -> J = erlang:trunc(R * (I + 1)), EI = erlang:element(I + 1, Arr), @@ -806,3 +817,20 @@ shuffle_loop(Randomizer, Arr, I) -> Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), shuffle_loop(Randomizer, Arr2, I - 1) end. + +randomizer_out_of_range(R) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => iolist_to_binary( + io_lib:format( + "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", + [R] + ) + ) + }). + +invalid_arg(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). diff --git a/src/fable-library-beam/fable_map.erl b/src/fable-library-beam/fable_map.erl index d5f257ea4c..7ab10c6338 100644 --- a/src/fable-library-beam/fable_map.erl +++ b/src/fable-library-beam/fable_map.erl @@ -74,7 +74,7 @@ iterate(Fn, Map) -> find_key(Fn, Map) -> case lists:dropwhile(fun({K, V}) -> not (Fn(K))(V) end, maps:to_list(Map)) of [{K, _} | _] -> K; - [] -> erlang:error(<<"key_not_found">>) + [] -> key_not_found(<<"An index satisfying the predicate was not found in the collection.">>) end. try_find_key(Fn, Map) -> @@ -109,7 +109,7 @@ try_get_value(Key, Map, OutRef) -> pick(Fn, Map) -> case try_pick(Fn, Map) of - undefined -> erlang:error(<<"key_not_found">>); + undefined -> key_not_found(<<"The given key was not present in the dictionary.">>); V -> V end. @@ -142,3 +142,9 @@ change(Key, Fn, Map) -> NewV -> maps:put(Key, NewV, Map) end end. + +key_not_found(Message) -> + erlang:error(#{ + exn_type => key_not_found_exception, + message => Message + }). diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index 447fd3cdb6..c9fdd1480f 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -101,9 +101,10 @@ type Native = static member firstWhere(xs: 'T[], predicate: 'T -> bool) : 'T = jsNative let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // https://stackoverflow.com/a/9113136 let reverseInPlace (xs: 'T[]) : unit = @@ -168,25 +169,25 @@ let map (f: 'T -> 'U) (source: 'T[]) : 'U[] = let mapIndexed2 (f: int -> 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f i source1[i] source2[i]) let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" Native.generate source1.Length (fun i -> f source1[i] source2[i]) let mapIndexed3 (f: int -> 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f i source1[i] source2[i] source3[i]) let map3 (f: 'T1 -> 'T2 -> 'T3 -> 'U) (source1: 'T1[]) (source2: 'T2[]) (source3: 'T3[]) : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" Native.generate source1.Length (fun i -> f source1[i] source2[i] source3[i]) @@ -344,7 +345,7 @@ let scanBack<'T, 'State> (folder: 'T -> 'State -> 'State) (array: 'T[]) (state: let skip count (array: 'T[]) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then Array.empty @@ -367,7 +368,7 @@ let take count (array: 'T[]) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then Array.empty @@ -423,7 +424,10 @@ let partition (f: 'T -> bool) (source: 'T[]) = Native.asFixed res1, Native.asFixed res2 -let find (predicate: 'T -> bool) (array: 'T[]) : 'T = Native.firstWhere (array, predicate) +let find (predicate: 'T -> bool) (array: 'T[]) : 'T = + match Native.indexWhere (array, predicate) with + | -1 -> indexNotFound () + | index -> array[index] let tryFind (predicate: 'T -> bool) (array: 'T[]) : 'T option = try @@ -546,14 +550,14 @@ let iterateIndexed (action: int -> 'T -> unit) (array: 'T[]) : unit = let iterate2 (action: 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1[i] array2[i] let iterateIndexed2 (action: int -> 'T1 -> 'T2 -> unit) (array1: 'T1[]) (array2: 'T2[]) : unit = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1[i] array2[i] @@ -570,7 +574,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res[j] <- x checkFlags[j] <- 1 @@ -580,7 +584,7 @@ let permute (f: int -> int) (array: 'T[]) : 'T[] = let isValid = checkFlags |> forAll ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -678,14 +682,14 @@ let unzip3 (array: ('a * 'b * 'c)[]) : 'a[] * 'b[] * 'c[] = let zip (array1: 'T[]) (array2: 'U[]) : ('T * 'U)[] = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" Native.generate array1.Length (fun i -> array1[i], array2[i]) let zip3 (array1: 'T1[]) (array2: 'T2[]) (array3: 'T3[]) : ('T1 * 'T2 * 'T3)[] = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" Native.generate array1.Length (fun i -> array1[i], array2[i], array3[i]) @@ -708,9 +712,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = Native.asFixed result let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + Native.sublist (array, 0, index), Native.sublist (array, index) // Note that, unlike the `compare` operator, Array.compareWith compares elements first, @@ -822,7 +829,7 @@ let fold2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder acc array1[i] array2[i] @@ -839,7 +846,7 @@ let foldBack2<'T1, 'T2, 'State> let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = array1.Length - 1 downto 0 do acc <- folder array1[i] array2[i] acc @@ -848,12 +855,14 @@ let foldBack2<'T1, 'T2, 'State> let reduce (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = // Dart's native reduce will fail if collection is empty - // if Array.isEmpty array then invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + if Array.isEmpty array then + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString + Native.reduce reduction array let reduceBack (reduction: 'T -> 'T -> 'T) (array: 'T[]) : 'T = if Array.isEmpty array then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString let mutable i = array.Length - 1 let mutable state = array[i] @@ -884,7 +893,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -970,7 +979,7 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let len = Operators.max 0 (source.Length - windowSize + 1) Native.generate len (fun i -> source[i .. i + windowSize - 1]) @@ -1017,7 +1026,7 @@ let transpose (arrays: 'T[] seq) : 'T[][] = let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" Native.generate lenInner (fun i -> Native.generate len (fun j -> arrays[j][i])) @@ -1138,7 +1147,7 @@ let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = int (r * float (i + 1)) let tmp = xs[i] @@ -1169,7 +1178,7 @@ let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] @@ -1193,7 +1202,7 @@ let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] ) @@ -1221,7 +1230,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = i + int (r * float (len - i)) let tmp = arr[i] diff --git a/src/fable-library-dart/Global.fs b/src/fable-library-dart/Global.fs index 0fcd96d1d0..5da4b222b5 100644 --- a/src/fable-library-dart/Global.fs +++ b/src/fable-library-dart/Global.fs @@ -16,7 +16,7 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." let inputSequenceEmpty = "The input sequence was empty." diff --git a/src/fable-library-dart/List.fs b/src/fable-library-dart/List.fs index d0cd36bb3b..50c0948e56 100644 --- a/src/fable-library-dart/List.fs +++ b/src/fable-library-dart/List.fs @@ -27,14 +27,17 @@ and [] LinkedList<'T>(tail_: 'T list member this.Head: 'T = match this with | :? ConsList<'T> as this -> this.GetHead() - | _ -> failwith "Empty list" + | _ -> invalidOp SR.inputWasEmpty member this.TryHead: 'T option = match this with | :? ConsList<'T> as this -> this.GetHead() |> Some | _ -> None - member _.Tail: LinkedList<'T> = Option.get tail + member _.Tail: LinkedList<'T> = + match tail with + | Some t -> t + | None -> invalidOp SR.inputWasEmpty member _.TryTail: LinkedList<'T> option = tail @@ -159,7 +162,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs with + | :? ConsList<'T> as xs -> xs.GetHead() + | _ -> invalidArg "list" SR.inputWasEmpty let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -167,7 +173,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.TryTail with + | Some t -> t + | None -> invalidArg "list" SR.inputWasEmpty let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -183,7 +192,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -572,13 +581,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -710,7 +719,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -730,7 +739,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -796,7 +805,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-dart/Map.fs b/src/fable-library-dart/Map.fs index 050ec50915..dc03ad26b2 100644 --- a/src/fable-library-dart/Map.fs +++ b/src/fable-library-dart/Map.fs @@ -607,9 +607,9 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/src/fable-library-dart/Range.fs b/src/fable-library-dart/Range.fs index 2e921701bd..75477dbe4e 100644 --- a/src/fable-library-dart/Range.fs +++ b/src/fable-library-dart/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T> (step: 'T) (stop: 'T) (zero: 'T) (add: 'T -> 'T -> let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index 36d5fdfb2f..dc22feb87f 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -944,13 +944,13 @@ let reduce (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = let arr = toArray xs if Array.isEmpty arr then - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty Array.reduceBack folder arr @@ -987,7 +987,7 @@ let skip (count: int) (source: seq<'T>) : seq<'T> = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1009,7 +1009,21 @@ let skipWhile (predicate: 'T -> bool) (xs: seq<'T>) : seq<'T> = ) ) -let tail (xs: seq<'T>) : seq<'T> = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) : seq<'T> = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take (count: int) (xs: seq<'T>) : seq<'T> = Enumerator.generateIndexed @@ -1019,7 +1033,7 @@ let take (count: int) (xs: seq<'T>) : seq<'T> = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-dart/Set.fs b/src/fable-library-dart/Set.fs index 6d24568e3b..0381ed456f 100644 --- a/src/fable-library-dart/Set.fs +++ b/src/fable-library-dart/Set.fs @@ -480,12 +480,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -515,9 +515,9 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = invalidOp SR.enumerationNotStarted - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = invalidOp SR.enumerationAlreadyFinished let current i = if i.started then diff --git a/src/fable-library-py/fable_library/List.fs b/src/fable-library-py/fable_library/List.fs index 509cf2d126..9859354847 100644 --- a/src/fable-library-py/fable_library/List.fs +++ b/src/fable-library-py/fable_library/List.fs @@ -48,12 +48,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -177,7 +177,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -185,7 +188,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -201,7 +207,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -596,13 +602,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -738,7 +744,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -758,7 +764,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -824,7 +830,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-py/fable_library/Map.fs b/src/fable-library-py/fable_library/Map.fs index 6416366633..c3368deb0a 100644 --- a/src/fable-library-py/fable_library/Map.fs +++ b/src/fable-library-py/fable_library/Map.fs @@ -593,9 +593,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -900,14 +902,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value diff --git a/src/fable-library-py/fable_library/Range.fs b/src/fable-library-py/fable_library/Range.fs index db1175c507..617f2472a8 100644 --- a/src/fable-library-py/fable_library/Range.fs +++ b/src/fable-library-py/fable_library/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 7991b5b6ad..e15b92cd11 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -880,7 +880,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -888,7 +888,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -923,7 +923,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -945,7 +945,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) // `take`/`takeWhile`: no F# impl - Replacements.fs redirects both straight to `seq_native`. diff --git a/src/fable-library-py/fable_library/Set.fs b/src/fable-library-py/fable_library/Set.fs index 7398b1d7de..0e326e35b1 100644 --- a/src/fable-library-py/fable_library/Set.fs +++ b/src/fable-library-py/fable_library/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py index 335ba69ebe..f9a9750ce4 100644 --- a/src/fable-library-py/fable_library/seq_native.py +++ b/src/fable-library-py/fable_library/seq_native.py @@ -26,6 +26,7 @@ from .list import try_item as list_try_item from .option import Option, some, value from .protocols import IEnumerable_1, IEnumerator +from .system import ArgumentException__ctor_Z384F8060, InvalidOperationException__ctor_Z721C83C5 from .types import UNIT, Unit from .util import Enumerator, get_enumerator, to_iterator @@ -165,7 +166,9 @@ def gen() -> Iterator[T]: for _ in range(count): v = next(it, _SENTINEL) if v is _SENTINEL: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'source')") + raise InvalidOperationException__ctor_Z721C83C5( + "The input sequence has an insufficient number of elements." + ) yield v return NativeSeq(gen) @@ -187,7 +190,7 @@ def try_head[T](xs: IEnumerable_1[T]) -> Option[T]: def head[T](xs: IEnumerable_1[T]) -> T: result = try_head(xs) if result is None: - raise Exception("The input sequence was empty. (Parameter 'source')") + raise ArgumentException__ctor_Z384F8060("The input sequence was empty.", "source") return value(result) @@ -213,7 +216,7 @@ def try_item[T](index: int, xs: IEnumerable_1[T]) -> Option[T]: def item[T](index: int, xs: IEnumerable_1[T]) -> T: result = try_item(index, xs) if result is None: - raise Exception("The input sequence has an insufficient number of elements. (Parameter 'index')") + raise ArgumentException__ctor_Z384F8060("The input sequence has an insufficient number of elements.", "index") return value(result) diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index 9632b11662..066ecf9e29 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -11,9 +11,10 @@ open Native open Native.Helpers let private indexNotFound () = - failwith "An index satisfying the predicate was not found in the collection." + raise (KeyNotFoundException(SR.keyNotFoundAlt)) -let private differentLengths () = failwith "Arrays had different lengths" +let private differentLengths (paramName: string) = + invalidArg paramName "The arrays have different lengths." // Pay attention when benchmarking to append and filter functions below // if implementing via native JS array .concat() and .filter() do not fall behind due to js-native transitions. @@ -77,7 +78,7 @@ let mapIndexed2 : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -88,7 +89,7 @@ let mapIndexed2 let map2 (f: 'T1 -> 'T2 -> 'U) (source1: 'T1[]) (source2: 'T2[]) ([] cons: Cons<'U>) : 'U[] = if source1.Length <> source2.Length then - failwith "Arrays had different lengths" + differentLengths "array1" let result = allocateArrayFromCons cons source1.Length @@ -106,7 +107,7 @@ let mapIndexed3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -124,7 +125,7 @@ let map3 : 'U[] = if source1.Length <> source2.Length || source2.Length <> source3.Length then - failwith "Arrays had different lengths" + differentLengths "array1, array2, array3" let result = allocateArrayFromCons cons source1.Length @@ -328,7 +329,7 @@ let scanBack<'T, 'State> folder (array: 'T[]) (state: 'State) ([] cons: Cons<'T>) = if count > array.Length then - invalidArg "count" "count is greater than array length" + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) if count = array.Length then allocateArrayFromCons cons 0 @@ -357,7 +358,7 @@ let take count (array: 'T[]) ([] cons: Cons<'T>) = invalidArg "count" LanguagePrimitives.ErrorStrings.InputMustBeNonNegativeString if count > array.Length then - invalidArg "count" "count is greater than array length" + invalidOp SR.notEnoughElements if count = 0 then allocateArrayFromCons cons 0 @@ -571,14 +572,14 @@ let iterateIndexed action (array: 'T[]) = let iterate2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action array1.[i] array2.[i] let iterateIndexed2 action (array1: 'T1[]) (array2: 'T2[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" for i = 0 to array1.Length - 1 do action i array1.[i] array2.[i] @@ -599,14 +600,15 @@ let forAll predicate (array: 'T[]) = let permute f (array: 'T[]) = let size = array.Length let res = copyImpl array - let checkFlags = allocateArray size + // Native `every` skips the empty slots of `new Array(size)` + let checkFlags = fillImpl (allocateArray size) 0 0 size iterateIndexed (fun i x -> let j = f i if j < 0 || j >= size then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res.[j] <- x checkFlags.[j] <- 1 @@ -616,7 +618,7 @@ let permute f (array: 'T[]) = let isValid = checkFlags |> forAllImpl ((=) 1) if not isValid then - invalidOp "Not a valid permutation" + invalidArg "indexMap" "The function did not compute a permutation." res @@ -719,7 +721,7 @@ let unzip3 (array: _[]) = let zip (array1: 'T[]) (array2: 'U[]) = // Shorthand version: map2 (fun x y -> x, y) array1 array2 if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let result = allocateArray array1.Length @@ -731,7 +733,7 @@ let zip (array1: 'T[]) (array2: 'U[]) = let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = // Shorthand version: map3 (fun x y z -> x, y, z) array1 array2 array3 if array1.Length <> array2.Length || array2.Length <> array3.Length then - differentLengths () + differentLengths "array1, array2, array3" let result = allocateArray array1.Length @@ -758,9 +760,12 @@ let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = result |> asArray let splitAt (index: int) (array: 'T[]) : 'T[] * 'T[] = - if index < 0 || index > array.Length then + if index < 0 then invalidArg "index" SR.indexOutOfBounds + if index > array.Length then + invalidOp SR.notEnoughElements + subArrayImpl array 0 index, skipImpl array index // Note that, though it's not consistent with `compare` operator, @@ -921,7 +926,7 @@ let foldIndexed2 folder state (array1: _[]) (array2: _[]) = let mutable acc = state if array1.Length <> array2.Length then - failwith "Arrays have different lengths" + differentLengths "array1" for i = 0 to array1.Length - 1 do acc <- folder i acc array1.[i] array2.[i] @@ -935,7 +940,7 @@ let foldBackIndexed2<'T1, 'T2, 'State> folder (array1: 'T1[]) (array2: 'T2[]) (s let mutable acc = state if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" let size = array1.Length @@ -949,7 +954,7 @@ let foldBack2<'T1, 'T2, 'State> f (array1: 'T1[]) (array2: 'T2[]) (state: 'State let reduce reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldIndexed (fun i acc x -> if i = 0 then x else reduction acc x) Unchecked.defaultof<_> array // else @@ -957,7 +962,7 @@ let reduce reduction (array: 'T[]) = let reduceBack reduction (array: 'T[]) = if array.Length = 0 then - invalidOp LanguagePrimitives.ErrorStrings.InputArrayEmptyString + invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString // if isTypedArrayImpl array then // foldBackIndexed (fun i x acc -> if i = 0 then x else reduction acc x) array Unchecked.defaultof<_> // else @@ -983,7 +988,7 @@ let rec existsOffset2 predicate (array1: _[]) (array2: _[]) index = let rec exists2 predicate (array1: _[]) (array2: _[]) = if array1.Length <> array2.Length then - differentLengths () + differentLengths "array1" existsOffset2 predicate array1 array2 0 @@ -1069,7 +1074,7 @@ let averageBy (projection: 'T -> 'T2) (array: 'T[]) ([] averager: IGener let windowed (windowSize: int) (source: 'T[]) : 'T[][] = if windowSize <= 0 then - failwith "windowSize must be positive" + invalidArg "windowSize" "The input must be positive." let res = FSharp.Core.Operators.max 0 (source.Length - windowSize + 1) |> allocateArray @@ -1120,7 +1125,7 @@ let transpose (arrays: 'T[] seq) ([] cons: Cons<'T>) : let lenInner = firstArray.Length if arrays |> forAll (fun a -> a.Length = lenInner) |> not then - differentLengths () + differentLengths "array.[0]" let result: 'T[][] = allocateArray lenInner @@ -1178,7 +1183,7 @@ let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = int (r * float (i + 1)) let tmp = xs[i] @@ -1209,7 +1214,7 @@ let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] @@ -1239,7 +1244,7 @@ let randomChoicesBy let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) xs[int (r * float len)] ) @@ -1274,7 +1279,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = let r = randomizer () if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) let j = i + int (r * float (len - i)) let tmp = arr[i] diff --git a/src/fable-library-ts/Global.fs b/src/fable-library-ts/Global.fs index 8b132814d3..416e179656 100644 --- a/src/fable-library-ts/Global.fs +++ b/src/fable-library-ts/Global.fs @@ -22,7 +22,7 @@ module SR = let indexOutOfBounds = "The index was outside the range of elements in the collection." - let inputWasEmpty = "Collection was empty." + let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." let inputSequenceEmpty = "The input sequence was empty." diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index e41e9724c7..bf29b18fa2 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -48,12 +48,12 @@ type LinkedList<'T> = member xs.Head = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some _ -> xs.head member xs.Tail = match xs.tail with - | None -> invalidArg "list" SR.inputWasEmpty + | None -> invalidOp SR.inputWasEmpty | Some t -> t member xs.Item @@ -186,7 +186,10 @@ let isEmpty (xs: 'T list) = xs.IsEmpty let length (xs: 'T list) = xs.Length -let head (xs: 'T list) = xs.Head +let head (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some _ -> xs.head let tryHead (xs: 'T list) = if xs.IsEmpty then @@ -194,7 +197,10 @@ let tryHead (xs: 'T list) = else Some xs.Head -let tail (xs: 'T list) = xs.Tail +let tail (xs: 'T list) = + match xs.tail with + | None -> invalidArg "list" SR.inputWasEmpty + | Some t -> t let rec tryLast (xs: 'T list) = if xs.IsEmpty then @@ -210,7 +216,7 @@ let rec tryLast (xs: 'T list) = let last (xs: 'T list) = match tryLast xs with | Some x -> x - | None -> failwith SR.inputWasEmpty + | None -> invalidArg "list" SR.inputWasEmpty let compareWith (comparer: 'T -> 'T -> int) (xs: 'T list) (ys: 'T list) : int = let rec loop (xs: 'T list) (ys: 'T list) = @@ -605,13 +611,13 @@ let replicate n x = initialize n (fun _ -> x) let reduce f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else fold f (head xs) (tail xs) let reduceBack f (xs: 'T list) = if xs.IsEmpty then - invalidOp SR.inputWasEmpty + invalidArg "list" SR.inputWasEmpty else foldBack f (tail xs) (head xs) @@ -747,7 +753,7 @@ let rec skip count (xs: 'T list) = if count <= 0 then xs elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) else skip (count - 1) xs.Tail @@ -767,7 +773,7 @@ let take count (xs: 'T list) = if i <= 0 then acc elif xs.IsEmpty then - invalidArg "list" SR.notEnoughElements + invalidOp SR.notEnoughElements else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail @@ -833,7 +839,7 @@ let splitAt index (xs: 'T list) = invalidArg "index" SR.inputMustBeNonNegative if index > xs.Length then - invalidArg "index" SR.notEnoughElements + invalidOp SR.notEnoughElements take index xs, skip index xs diff --git a/src/fable-library-ts/Map.fs b/src/fable-library-ts/Map.fs index ee52a22643..97d8f177fc 100644 --- a/src/fable-library-ts/Map.fs +++ b/src/fable-library-ts/Map.fs @@ -592,9 +592,11 @@ module MapTree = started = false } - let notStarted () = failwith "enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "enumeration already finished" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then @@ -905,14 +907,14 @@ type Map<[] 'Key, [> with member m.Add x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Clear() = - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Remove x = ignore x - raise (System.NotSupportedException("Map cannot be mutated")) + raise (System.NotSupportedException("Map values cannot be mutated.")) member m.Contains x = m.ContainsKey x.Key && Unchecked.equals m.[x.Key] x.Value @@ -935,11 +937,11 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key, p.Value) @@ -948,7 +950,7 @@ type Map<[] 'Key, [ Seq.map (fun p -> p.Key) member m.set(k, v) = - failwith "Map cannot be mutated" + raise (System.NotSupportedException("Map values cannot be mutated.")) m :> JS.Map<'Key, 'Value> member m.values() = m |> Seq.map (fun p -> p.Value) diff --git a/src/fable-library-ts/Range.fs b/src/fable-library-ts/Range.fs index d6ce3d8933..eeaa35e429 100644 --- a/src/fable-library-ts/Range.fs +++ b/src/fable-library-ts/Range.fs @@ -4,7 +4,7 @@ let makeRangeStepFunction<'T when 'T: comparison> (step: 'T) (stop: 'T) (zero: ' let stepComparedWithZero = compare step zero if stepComparedWithZero = 0 then - failwith "The step of a range cannot be zero" + invalidArg "step" "The step of a range cannot be zero." let stepGreaterThanZero = stepComparedWithZero > 0 diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index 20631e4a89..fe33447e9a 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -991,7 +991,7 @@ let reduce folder (xs: seq<'T>) = if e.MoveNext() then loop e.Current else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let reduceBack folder (xs: seq<'T>) = let arr = toArray xs @@ -999,7 +999,7 @@ let reduceBack folder (xs: seq<'T>) = if arr.Length > 0 then Array.reduceBack folder arr else - invalidOp SR.inputSequenceEmpty + invalidArg "source" SR.inputSequenceEmpty let replicate n x = initialize n (fun _ -> x) @@ -1034,7 +1034,7 @@ let skip count (source: seq<'T>) = try for _ = 1 to count do if not (e.MoveNext()) then - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements let compensation () = () Enumerator.enumerateThenFinally compensation e @@ -1056,7 +1056,21 @@ let skipWhile predicate (xs: seq<'T>) = ) ) -let tail (xs: seq<'T>) = skip 1 xs +// FSharp.Core's Seq.tail raises ArgumentException on an empty input, Seq.skip InvalidOperationException +let tail (xs: seq<'T>) = + mkSeq (fun () -> + let e = ofSeq xs + + try + if not (e.MoveNext()) then + invalidArg "source" SR.notEnoughElements + + let compensation () = () + Enumerator.enumerateThenFinally compensation e + with _ -> + e.Dispose() + reraise () + ) let take count (xs: seq<'T>) = generateIndexed @@ -1066,7 +1080,7 @@ let take count (xs: seq<'T>) = if e.MoveNext() then Some(e.Current) else - invalidArg "source" SR.notEnoughElements + invalidOp SR.notEnoughElements else None ) diff --git a/src/fable-library-ts/Set.fs b/src/fable-library-ts/Set.fs index 4a39504015..ac222fc327 100644 --- a/src/fable-library-ts/Set.fs +++ b/src/fable-library-ts/Set.fs @@ -482,12 +482,12 @@ module SetTree = let minimumElement s = match minimumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." let maximumElement s = match maximumElementOpt s with | Some k -> k - | None -> failwith "Set contains no elements" + | None -> invalidArg "s" "Set contains no elements." // Imperative left-to-right iterators. [] @@ -517,9 +517,11 @@ module SetTree = started = false } - let notStarted () = failwith "Enumeration not started" + let notStarted () = + invalidOp "Enumeration has not started. Call MoveNext." - let alreadyFinished () = failwith "Enumeration already started" + let alreadyFinished () = + invalidOp "Enumeration already finished." let current i = if i.started then diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 9b4544b7d5..8fbd898d6d 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -1343,3 +1343,32 @@ let ``test byte array from literal works`` () = data.[1] |> equal 2uy data.[2] |> equal 3uy data.Length |> equal 3 + +[] +let ``test Array.take raises InvalidOperationException when the array is too short`` () = + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + +[] +let ``test Array.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) [|1|]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun _ -> None: int option) [|1|]) |> equal "KeyNotFoundException" + +[] +let ``test Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> -0.1) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Array random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> Array.randomChoice [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomChoices 1 [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 3 [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomSample 1 [||]) |> equal "ArgumentException" diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index 2733453992..498beca6e1 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -906,3 +906,37 @@ let ``test List.range works`` () = [0..2..9] |> List.reduce (+) |> equal 20 + +[] +let ``test List.take raises InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + +[] +let ``test List.exactlyOne raises ArgumentException unless the list has one element`` () = + raisedExceptionType (fun () -> List.exactlyOne []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exactlyOne [1; 2]) |> equal "ArgumentException" + List.exactlyOne [1] |> equal 1 + +[] +let ``test List.find and friends raise KeyNotFoundException when nothing matches`` () = + raisedExceptionType (fun () -> List.find (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndex (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.findIndexBack (fun x -> x > 5) [1]) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> List.pick (fun _ -> None: int option) [1]) |> equal "KeyNotFoundException" + +[] +let ``test List random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test List random functions raise ArgumentException for invalid inputs`` () = + raisedExceptionType (fun () -> List.randomChoice []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomChoices 1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 3 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomSample 1 []) |> equal "ArgumentException" diff --git a/tests/Beam/MapTests.fs b/tests/Beam/MapTests.fs index 68c4fcf07a..a99b869105 100644 --- a/tests/Beam/MapTests.fs +++ b/tests/Beam/MapTests.fs @@ -409,3 +409,9 @@ let ``test Map.toSeq works`` () = let zs = Map.toSeq ys (Seq.item 2 xs |> snd) = (Seq.item 2 zs |> snd) |> equal true + +[] +let ``test Map.findKey and Map.pick raise KeyNotFoundException when nothing matches`` () = + let m = Map.ofList [ (1, "a"); (2, "b") ] + raisedExceptionType (fun () -> Map.findKey (fun k _ -> k > 5) m) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Map.pick (fun _ _ -> None: int option) m) |> equal "KeyNotFoundException" diff --git a/tests/Beam/SeqTests.fs b/tests/Beam/SeqTests.fs index 1e43a350b3..ea637949d3 100644 --- a/tests/Beam/SeqTests.fs +++ b/tests/Beam/SeqTests.fs @@ -1161,3 +1161,37 @@ let ``test Seq.toList on string works`` () = let text = "ABC" let chars = text |> Seq.toList chars |> List.length |> equal 3 + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.reduce (+) Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.min and friends raise ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.min Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException past the end`` () = + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on an empty sequence`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range`` () = + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Range expressions raise ArgumentException for a zero step`` () = + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1L .. 0L .. 5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1.0 .. 0.0 .. 5.0]) |> equal "ArgumentException" diff --git a/tests/Beam/Util.fs b/tests/Beam/Util.fs index 279a109b8c..28fd76e22c 100644 --- a/tests/Beam/Util.fs +++ b/tests/Beam/Util.fs @@ -69,3 +69,15 @@ let throwsAnyError (f: unit -> 'a): unit = let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess + +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | e -> "Unexpected exception: " + e.Message diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index 54aaa76232..a2a02efb42 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -1328,6 +1328,61 @@ let tests () = equal c2 -1 equal c3 1 + testCase "Array.reduce and Array.min on an empty array raise ArgumentException" <| fun () -> + let xs: int[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id xs) |> equal "ArgumentException" + + testCase "Array.take and Array.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + + testCase "Array functions on arrays of different lengths raise ArgumentException" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (=) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array.find and Array.pick raise KeyNotFoundException when nothing matches" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 5) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 5 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array.windowed and Array.permute with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 5) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + + testCase "Array random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + // testCase "System.Array.Resize works" <| fun () -> // let mutable xs = [|1; 2; 3; 4; 5|] // System.Array.Resize(&xs, 3) diff --git a/tests/Dart/src/ListTests.fs b/tests/Dart/src/ListTests.fs index 58cd87c5df..acdde1fa7d 100644 --- a/tests/Dart/src/ListTests.fs +++ b/tests/Dart/src/ListTests.fs @@ -1063,3 +1063,46 @@ let tests() = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List.reduce and List.min on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List.head, List.tail and List.last on an empty list raise ArgumentException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + + testCase "list.Head and list.Tail on an empty list raise InvalidOperationException" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List.take and List.splitAt past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List.skip past the end raises ArgumentOutOfRangeException" <| fun () -> + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 5) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [2; 3]]) |> equal "ArgumentException" + + testCase "List random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 1 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" diff --git a/tests/Dart/src/MapTests.fs b/tests/Dart/src/MapTests.fs index 68161e9c95..366f6b9dab 100644 --- a/tests/Dart/src/MapTests.fs +++ b/tests/Dart/src/MapTests.fs @@ -295,3 +295,11 @@ let tests() = |> Map.add { Bar = "a"; Foo = 10 } 2 |> Map.count |> equal 1 + + testCase "Map enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Map.ofList [(1, "a")] :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current.Key |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/SeqTests.fs b/tests/Dart/src/SeqTests.fs index ce53aaf7c8..795c9e297e 100644 --- a/tests/Dart/src/SeqTests.fs +++ b/tests/Dart/src/SeqTests.fs @@ -1163,3 +1163,36 @@ let tests() = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + + testCase "Seq.reduce and Seq.min on an empty sequence raise ArgumentException" <| fun () -> + let xs: int seq = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + + testCase "Seq.take and Seq.skip past the end raise InvalidOperationException" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq.tail on an empty sequence raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + Seq.tail [1; 2; 3] |> Seq.toList |> equal [2; 3] + + testCase "Seq functions with invalid arguments raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 5) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq random functions raise ArgumentOutOfRangeException when the randomizer is out of range" <| fun () -> + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomChoicesBy (fun () -> 1.0) 1 [1] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|1L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [1. .. 0. .. 5.]) |> equal "ArgumentException" diff --git a/tests/Dart/src/SetTests.fs b/tests/Dart/src/SetTests.fs index 21d9ea6fb0..bd1b5c7241 100644 --- a/tests/Dart/src/SetTests.fs +++ b/tests/Dart/src/SetTests.fs @@ -289,3 +289,21 @@ let tests() = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + + testCase "Set.minElement and Set.maxElement on an empty set raise ArgumentException" <| fun () -> + let xs: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + + testCase "Set.intersectMany with no sets raises ArgumentException" <| fun () -> + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current outside the enumeration raises InvalidOperationException" <| fun () -> + let e = (Set.ofList [1] :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + e.MoveNext() |> equal true + e.Current |> equal 1 + e.MoveNext() |> equal false + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index 4e0754c735..d821dfb7b0 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -22,6 +22,18 @@ let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) let throwsAnyError (f: unit -> 'a): unit = Test.expect(f, Test.throwsException) +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | :? System.InvalidOperationException -> "InvalidOperationException" + | :? System.Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | e -> "Unexpected exception: " + e.Message + let private run (f: unit -> 'a) = try f() diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index b0b90a14a6..0e97aad4f6 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -1526,4 +1526,58 @@ let tests = coll.CopyTo(ys, 0) ys = xs |> equal true + testCase "Array functions raise ArgumentException on empty arrays" <| fun () -> + let xs: int[] = [||] + let ys: string[] = [||] + raisedExceptionType (fun () -> Array.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduce (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.reduceBack (+) ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.Parallel.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.max ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.maxBy id ys) |> equal "ArgumentException" + + testCase "Array functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> Array.take 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.take -1 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.skip 5 [|1|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.splitAt 5 [|1|]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Array.splitAt -1 [|1|]) |> equal "ArgumentException" + + testCase "Array functions raise ArgumentException for arrays of different lengths" <| fun () -> + let xs = [|1|] + let ys = [|1; 2|] + raisedExceptionType (fun () -> Array.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.zip3 xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.mapi2 (fun i x y -> i + x + y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.map3 (fun x y z -> x + y + z) xs ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.foldBack2 (fun x y acc -> x + y + acc) xs ys 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.exists2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [| xs; ys |]) |> equal "ArgumentException" + + testCase "Array functions raise KeyNotFoundException when nothing satisfies the predicate" <| fun () -> + let xs = [|1; 2|] + raisedExceptionType (fun () -> Array.find (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndex (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.findIndexBack (fun x -> x > 2) xs) |> equal "KeyNotFoundException" + raisedExceptionType (fun () -> Array.pick (fun x -> if x > 2 then Some x else None) xs) |> equal "KeyNotFoundException" + + testCase "Array functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Array.windowed 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun i -> i + 1) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.permute (fun _ -> 0) [|1; 2|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.randomShuffleInPlaceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomShuffleBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 2 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + ] diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index d5f9a68272..4df3113faf 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1075,4 +1075,41 @@ let tests = List.distinct sample |> List.length |> equal 3 throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + + testCase "List functions raise ArgumentException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + + testCase "List Head and Tail properties raise InvalidOperationException on empty lists" <| fun () -> + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + + testCase "List functions raise FSharp.Core's exception types for out of range counts" <| fun () -> + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + + testCase "List functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> List.windowed 0 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1]; [1; 2]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun i -> i + 1) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.permute (fun _ -> 0) [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.randomShuffleBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 2 [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" ] diff --git a/tests/Js/Main/MapTests.fs b/tests/Js/Main/MapTests.fs index eee708d352..caa3d043e1 100644 --- a/tests/Js/Main/MapTests.fs +++ b/tests/Js/Main/MapTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Maps open System.Collections.Generic open Util.Testing +open Fable.Tests.Util type R = { i: int; s: string } type R2 = { kv: KeyValuePair } @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] |> Array.map KeyValuePair coll.CopyTo(ys, 0) ys = (xs |> Array.map KeyValuePair) |> equal true + + testCase "Map enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let m = Map [1, "a"; 2, "b"] + let e = (m :> seq>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + + testCase "Map raises NotSupportedException when mutated through IDictionary or ICollection" <| fun () -> + let dic = Map [1, "a"] :> IDictionary + let coll = Map [1, "a"] :> ICollection> + raisedExceptionType (fun () -> dic.Add(2, "b")) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.Remove(1)) |> equal "NotSupportedException" + raisedExceptionType (fun () -> dic.[2] <- "b") |> equal "NotSupportedException" + raisedExceptionType (fun () -> coll.Clear()) |> equal "NotSupportedException" ] diff --git a/tests/Js/Main/SeqTests.fs b/tests/Js/Main/SeqTests.fs index 620324d219..fc19f494ba 100644 --- a/tests/Js/Main/SeqTests.fs +++ b/tests/Js/Main/SeqTests.fs @@ -568,6 +568,13 @@ let tests = |> Seq.reduce (+) |> equal 19990000000000002000I + testCase "Ranges with a zero step raise ArgumentException" <| fun () -> + let step = 0 + raisedExceptionType (fun () -> [0 .. step .. 5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [|0L .. 0L .. 5L|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0. .. 0. .. 1.]) |> equal "ArgumentException" + raisedExceptionType (fun () -> seq {0M .. 0M .. 1M} |> Seq.toList) |> equal "ArgumentException" + testCase "Seq.reduce works" <| fun () -> let xs = [1.; 2.] xs |> Seq.reduce (+) @@ -1162,4 +1169,25 @@ let tests = throwsAnyError (fun () -> Seq.removeManyAt 0 2 [] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt -1 2 [1] |> Seq.toList |> ignore) throwsAnyError (fun () -> Seq.removeManyAt 2 2 [1] |> Seq.toList |> ignore) + + testCase "Seq functions raise ArgumentException on empty sequences" <| fun () -> + let xs = seq { for x in [1; 2] do if x > 2 then yield x } + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.tail xs |> Seq.toList) |> equal "ArgumentException" + + testCase "Seq functions raise InvalidOperationException when there are not enough elements" <| fun () -> + raisedExceptionType (fun () -> Seq.take 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 [1] |> Seq.toList) |> equal "InvalidOperationException" + + testCase "Seq functions raise FSharp.Core's exception types for invalid arguments" <| fun () -> + raisedExceptionType (fun () -> Seq.windowed 0 [1] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun i -> i + 1) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" ] diff --git a/tests/Js/Main/SetTests.fs b/tests/Js/Main/SetTests.fs index 2f752a51e5..88e36cc8d1 100644 --- a/tests/Js/Main/SetTests.fs +++ b/tests/Js/Main/SetTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Sets open System.Collections.Generic open Util.Testing +open Fable.Tests.Util let tests = testList "Sets" [ @@ -320,4 +321,19 @@ let tests = let ys = [| ("D", 4); ("E", 5); ("F", 6) |] coll.CopyTo(ys, 0) ys = xs |> equal true + + testCase "Set functions raise ArgumentException on empty sets" <| fun () -> + let s: Set = Set.empty + raisedExceptionType (fun () -> Set.minElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement s) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> s.MaximumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + + testCase "Set enumerator Current raises InvalidOperationException outside the enumeration" <| fun () -> + let s = set [1; 2] + let e = (s :> seq).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" ] diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index da64962240..e0c24a8220 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -108,6 +108,19 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? NotSupportedException -> "NotSupportedException" + | e -> "Unexpected exception: " + e.Message + let rec sumFirstSeq (zs: seq) (n: int): float = match n with | 0 -> 0. diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs index 05badb8fa9..c7db8d1859 100644 --- a/tests/Python/TestList.fs +++ b/tests/Python/TestList.fs @@ -1246,3 +1246,43 @@ let ``test List.collect works II`` () = xs |> List.collect (fun a -> [a.Length]) |> equal [1; 5; 3] + +[] +let ``test List.reduce and List.reduceBack raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test List.min, List.max, List.minBy and List.maxBy raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test List.last, List.head and List.tail raise ArgumentException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> List.last xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.tail xs) |> equal "ArgumentException" + +[] +let ``test list.Head and list.Tail raise InvalidOperationException on empty input`` () = + let xs: int list = [] + raisedExceptionType (fun () -> xs.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> xs.Tail) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + +[] +let ``test List.take and List.splitAt raise InvalidOperationException when the list is too short`` () = + raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.splitAt 5 [1]) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitAt -1 [1]) |> equal "ArgumentException" + +[] +let ``test List.skip raises ArgumentOutOfRangeException when the list is too short`` () = + raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" diff --git a/tests/Python/TestMap.fs b/tests/Python/TestMap.fs index 4c33e1d710..bee7396796 100644 --- a/tests/Python/TestMap.fs +++ b/tests/Python/TestMap.fs @@ -1,6 +1,7 @@ module Fable.Tests.Maps open System.Collections.Generic +open Fable.Tests.Util open Util.Testing type R = { i: int; s: string } @@ -351,3 +352,10 @@ let ``test Map keys and values work correctly`` () = let myMap = Map [ "a", 10; "b", 20 ] :> IDictionary<_,_> myMap.Keys |> Seq.toList |> equal ["a"; "b"] myMap.Values |> Seq.toList |> equal [10; 20] + +[] +let ``test Map enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (Map [1, "a"] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs index 35a4ff6f8f..78314876de 100644 --- a/tests/Python/TestSeq.fs +++ b/tests/Python/TestSeq.fs @@ -1319,3 +1319,42 @@ let ``test Seq.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + +[] +let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.reduceBack (+) xs) |> equal "ArgumentException" + +[] +let ``test Seq.min, Seq.max, Seq.minBy and Seq.maxBy raise ArgumentException on empty input`` () = + let xs = Seq.empty + raisedExceptionType (fun () -> Seq.min xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.max xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.minBy id xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.maxBy id xs) |> equal "ArgumentException" + +[] +let ``test Seq.take and Seq.skip raise InvalidOperationException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.take 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + raisedExceptionType (fun () -> Seq.skip 5 (seq { 1 }) |> Seq.toList) |> equal "InvalidOperationException" + +[] +let ``test Seq.tail raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Seq.tail Seq.empty |> Seq.toList) |> equal "ArgumentException" + +[] +let ``test range with a zero step raises ArgumentException`` () = + raisedExceptionType (fun () -> [0..0..5]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0L..0L..5L]) |> equal "ArgumentException" + raisedExceptionType (fun () -> [0.0..0.0..5.0]) |> equal "ArgumentException" + +[] +let ``test Seq.head and Seq.item raise ArgumentException when the sequence is too short`` () = + raisedExceptionType (fun () -> Seq.head Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([]: int list)) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.head ([||]: int[])) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 (seq { 1 })) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item 5 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.item -1 (seq { 1 })) |> equal "ArgumentException" diff --git a/tests/Python/TestSet.fs b/tests/Python/TestSet.fs index df67710256..fe5e9956d1 100644 --- a/tests/Python/TestSet.fs +++ b/tests/Python/TestSet.fs @@ -1,5 +1,6 @@ module Fable.Tests.Set +open Fable.Tests.Util open Util.Testing [] @@ -333,3 +334,22 @@ let ``test Comparing large sets works`` () = largeSetB.IsSupersetOf(largeSetA) |> equal true largeSetA.IsProperSubsetOf(largeSetB) |> equal false largeSetA.IsSubsetOf(largeSetB) |> equal true + +[] +let ``test Set.minElement and Set.maxElement raise ArgumentException on empty input`` () = + let xs = Set.empty + raisedExceptionType (fun () -> Set.minElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Set.maxElement xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MinimumElement) |> equal "ArgumentException" + raisedExceptionType (fun () -> xs.MaximumElement) |> equal "ArgumentException" + +[] +let ``test Set.intersectMany raises ArgumentException on empty input`` () = + raisedExceptionType (fun () -> Set.intersectMany ([]: Set list)) |> equal "ArgumentException" + +[] +let ``test Set enumerator Current raises InvalidOperationException outside the enumeration`` () = + let e = (set [1] :> seq<_>).GetEnumerator() + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" + while e.MoveNext() do () + raisedExceptionType (fun () -> e.Current) |> equal "InvalidOperationException" diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs index e80a7e9561..72637b7aa4 100644 --- a/tests/Python/Util.fs +++ b/tests/Python/Util.fs @@ -106,6 +106,17 @@ let doesntThrow (f: unit -> 'a): unit = run f |> RunResult.wasSuccess +/// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. +let raisedExceptionType (f: unit -> 'a): string = + try + f () |> ignore + "no exception" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | :? InvalidOperationException -> "InvalidOperationException" + | e -> "Unexpected exception: " + e.Message + let Int32Array = [|1;2|] module Float64Array = From 04ddbd2027e03f4cf7b0b22fe886fdabadf26d93 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 21:16:57 +0100 Subject: [PATCH 235/322] fix(js/ts): format DateTimeOffset in its own offset (#5171) --- src/fable-library-ts/Date.ts | 2 +- tests/Js/Main/DateTimeOffsetTests.fs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/fable-library-ts/Date.ts b/src/fable-library-ts/Date.ts index c585623b14..5fa3bb9d0e 100644 --- a/src/fable-library-ts/Date.ts +++ b/src/fable-library-ts/Date.ts @@ -403,7 +403,7 @@ function dateToISOStringWithOffset(dateWithOffset: Date, offset: number) { } function dateToStringWithOffset(date: IDateTimeOffset, format?: string) { - const d = new Date(date.getTime() + (date.offset ?? 0)); + const d = DateTime(date.getTime() + (date.offset ?? 0), DateTimeKind.Utc); if (typeof format !== "string") { return d.toISOString().replace(/\.\d+/, "").replace(/[A-Z]|\.\d+/g, " ") + dateOffsetToString((date.offset ?? 0)); } else if (format.length === 1) { diff --git a/tests/Js/Main/DateTimeOffsetTests.fs b/tests/Js/Main/DateTimeOffsetTests.fs index c52e7557ab..283844a0a6 100644 --- a/tests/Js/Main/DateTimeOffsetTests.fs +++ b/tests/Js/Main/DateTimeOffsetTests.fs @@ -112,6 +112,23 @@ let tests = DateTimeOffset(2014, 9, 1, 16, 37, 2, TimeSpan.Zero).ToString("s", CultureInfo.InvariantCulture) |> equal "2014-09-01T16:37:02" + testCase "DateTimeOffset.ToString uses the offset's date in every local timezone" <| fun _ -> + // At +14 and -12 the offset's date differs from the UTC date, so one of them differs from any local date + let early = DateTimeOffset(2014, 9, 1, 0, 30, 5, TimeSpan.FromHours 14) + let late = DateTimeOffset(2014, 9, 1, 23, 30, 5, TimeSpan.FromHours -12) + for d in [ early; late ] do + let format (f: string) = d.ToString(f, CultureInfo.InvariantCulture) + let time = if d = early then "00:30" else "23:30" + format "D" |> equal "Monday, 01 September 2014" + format "d" |> equal "09/01/2014" + format "F" |> equal ("Monday, 01 September 2014 " + time + ":05") + format "f" |> equal ("Monday, 01 September 2014 " + time) + format "G" |> equal ("09/01/2014 " + time + ":05") + format "g" |> equal ("09/01/2014 " + time) + format "M" |> equal "September 01" + format "Y" |> equal "2014 September" + format "dd/MM/yyyy HH:mm" |> equal ("01/09/2014 " + time) + testCase "DateTimeOffset from Year 1 to 99 works" <| fun () -> let date = DateTimeOffset(1, 1, 2, 0, 0, 0, TimeSpan.Zero) date.Year |> equal 1 From 401c881f729494d06d20da9687e78f7315f5efb2 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 22:04:49 +0100 Subject: [PATCH 236/322] fix(python/beam/rust): implicit DateTime to DateTimeOffset (#5173) --- src/Fable.Transforms/Beam/Replacements.fs | 3 +++ src/Fable.Transforms/Python/Replacements.fs | 3 +++ src/Fable.Transforms/Rust/Replacements.fs | 1 + tests/Beam/DateTimeOffsetTests.fs | 13 +++++++++++++ tests/Python/TestDateTimeOffset.fs | 13 +++++++++++++ tests/Rust/tests/src/DateTimeOffsetTests.fs | 13 +++++++++++++ 6 files changed, 46 insertions(+) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 4b3df7f340..22d086f994 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -5036,6 +5036,9 @@ let private dateTimeOffsets (args: Expr list) = match info.CompiledName with + | "op_Implicit" -> + Helper.LibCall(com, "fable_date_offset", "from_date", t, args @ [ makeIntConst 0 ], ?loc = r) + |> Some | ".ctor" -> match args with | [] -> Helper.LibCall(com, "fable_date_offset", "min_value", t, [], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 559fe0176a..9ed7449bd7 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3190,6 +3190,9 @@ let dates (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio "DateOffset" match i.CompiledName with + | "op_Implicit" -> + Helper.LibCall(com, "DateOffset", "fromDate", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | ".ctor" -> match args with | [] -> Helper.LibCall(com, moduleName, "minValue", t, [], [], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 91add9cd16..f02f077db3 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -2652,6 +2652,7 @@ let dateTimeOffsets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: _offset ] -> "new_ymdhms_micro" |> Some | _ -> None |> Option.map (fun meth -> makeStaticMemberCall com r t i "DateTimeOffset" meth args) + | "op_Implicit", _ -> makeStaticMemberCall com r t i "DateTimeOffset" "new_datetime" args |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg -> valueTypes com ctx r t i thisArg args | "Add", Some c -> Operation(Binary(BinaryOperator.BinaryPlus, c, args.Head), Tags.empty, t, r) diff --git a/tests/Beam/DateTimeOffsetTests.fs b/tests/Beam/DateTimeOffsetTests.fs index 64eb006b15..e791c81ca9 100644 --- a/tests/Beam/DateTimeOffsetTests.fs +++ b/tests/Beam/DateTimeOffsetTests.fs @@ -518,3 +518,16 @@ let ``test DateTimeOffset constructor from DateOnly and TimeOnly works`` () = dt.Minute |> equal dt_exp.Minute dt.Second |> equal dt_exp.Second dt.Millisecond |> equal dt_exp.Millisecond + +[] +let ``test Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.Year |> equal 2024 + dto.Month |> equal 1 + dto.Day |> equal 15 + dto.Hour |> equal 13 + dto.Minute |> equal 23 + dto.Second |> equal 30 diff --git a/tests/Python/TestDateTimeOffset.fs b/tests/Python/TestDateTimeOffset.fs index c4ea6ac15c..af806354cc 100644 --- a/tests/Python/TestDateTimeOffset.fs +++ b/tests/Python/TestDateTimeOffset.fs @@ -569,3 +569,16 @@ let ``test DateTimeOffset constructor from DateOnly and TimeOnly works`` () = dt.Minute |> equal dt_exp.Minute dt.Second |> equal dt_exp.Second dt.Millisecond |> equal dt_exp.Millisecond + +[] +let ``test Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.UtcDateTime.Year |> equal 2024 + dto.UtcDateTime.Month |> equal 1 + dto.UtcDateTime.Day |> equal 15 + dto.UtcDateTime.Hour |> equal 13 + dto.UtcDateTime.Minute |> equal 23 + dto.UtcDateTime.Second |> equal 30 diff --git a/tests/Rust/tests/src/DateTimeOffsetTests.fs b/tests/Rust/tests/src/DateTimeOffsetTests.fs index e0f13dd684..d95c8c5b67 100644 --- a/tests/Rust/tests/src/DateTimeOffsetTests.fs +++ b/tests/Rust/tests/src/DateTimeOffsetTests.fs @@ -1031,3 +1031,16 @@ let ``-10s offset throws exception`` () = let source = DateTimeOffset(2007, 9, 1, 14, 30, 0, TimeSpan.Zero) (fun () -> source.ToOffset(TimeSpan(0, 0, -10))) |> throwsErrorContaining "Offset must be specified in whole minutes" + +[] +let ``Implicit conversion from DateTime preserves value`` () = + let d = DateTime(2024, 1, 15, 13, 23, 30, DateTimeKind.Utc) + let dto: DateTimeOffset = d + dto |> equal (DateTimeOffset(d)) + dto.Offset |> equal TimeSpan.Zero + dto.Year |> equal 2024 + dto.Month |> equal 1 + dto.Day |> equal 15 + dto.Hour |> equal 13 + dto.Minute |> equal 23 + dto.Second |> equal 30 From 9c3516268674b54aec2efae381cad920d4f15922 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 22:18:53 +0100 Subject: [PATCH 237/322] fix(js/ts/python): compare ReferenceEquality types by ref (#5174) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 53 +++++++++++ .../Python/Fable2Python.Transforms.fs | 2 + .../Python/Fable2Python.Util.fs | 40 ++++++++ tests/Js/Main/ComparisonTests.fs | 76 ++++++++++++++++ tests/Js/Main/HashSetTests.fs | 18 ++-- tests/Python/TestComparison.fs | 91 +++++++++++++++++++ tests/Python/TestHashSet.fs | 20 ++-- 7 files changed, 281 insertions(+), 19 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 431293cc85..c76713ca33 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -3809,6 +3809,57 @@ but thanks to the optimisation done below we get ModuleDecl(entName + Naming.reflectionSuffix, isPublic = ent.IsPublic) |> declareModuleMember com ctx fnExpr + let hasClassMethod name (classMembers: ClassMember array) = + classMembers + |> Array.exists ( + function + | ClassMethod(ClassFunction(Expression.Identifier(Identifier(key, _)), _), _, _, _, _, _, _, _, _) -> + key = name + | _ -> false + ) + + let withReferenceEquality (com: IBabelCompiler) ctx (ent: Fable.Entity) (classMembers: ClassMember array) = + let isReferenceEqualityType = + (ent.IsFSharpRecord || ent.IsFSharpUnion) + && not ent.IsValueType + && not (FSharp2Fable.Util.hasStructuralEquality ent) + + if not isReferenceEqualityType then + classMembers + else + let ifTypeScript annotation = + if com.IsTypeScript then + Some annotation + else + None + + let makeMethod name parameters returnExpr returnType = + ClassMember.classMethod ( + ClassFunction(Expression.identifier name, false), + parameters, + Some(BlockStatement [| Statement.returnStatement returnExpr |]), + ?returnType = ifTypeScript returnType + ) + + [| + yield! classMembers + if not (hasClassMethod "Equals" classMembers) then + let other = + Parameter.parameter ("other", ?typeAnnotation = ifTypeScript AnyTypeAnnotation) + + makeMethod + "Equals" + [| other |] + (Expression.binaryExpression (BinaryEqual, thisExpr, Expression.identifier other.Name)) + BooleanTypeAnnotation + if not (hasClassMethod "GetHashCode" classMembers) then + makeMethod + "GetHashCode" + [||] + (libCall com ctx None "Util" "physicalHash" [] [ thisExpr ]) + NumberTypeAnnotation + |] + let declareType (com: IBabelCompiler) ctx @@ -4034,6 +4085,7 @@ but thanks to the optimisation done below we get (doc: string option) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers let isPublic = ent.IsPublic let tagArgName = "Tag" let tagArgTa = makeAliasTypeAnnotation com ctx tagArgName @@ -4339,6 +4391,7 @@ but thanks to the optimisation done below we get (decl: Fable.ClassDecl) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers let fieldIds = getEntityFieldsAsIdents ent |> List.toArray let args = fieldIds |> Array.map identAsExpr diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 4581313a36..6a626cb5b5 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -4297,6 +4297,7 @@ let transformAttachedMethod (com: IPythonCompiler) ctx (info: Fable.MemberFuncti ] let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: string) classMembers = + let classMembers = withReferenceEquality com ctx ent classMembers // Get generic type parameters for the union let typeParams = makeEntityTypeParams com ctx ent @@ -4501,6 +4502,7 @@ let transformClassWithCompilerGeneratedConstructor (classAttributes: ClassAttributes) = // printfn "transformClassWithCompilerGeneratedConstructor" + let classMembers = withReferenceEquality com ctx ent classMembers let fieldIds = getEntityFieldsAsIdents com ent let args = diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 566f277627..23bfd37303 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -694,6 +694,46 @@ module Util = let thisExpr = Expression.name "self" + let hasClassMethod name (classMembers: Statement list) = + classMembers + |> List.exists ( + function + | FunctionDef { Name = Identifier key } -> key = name + | _ -> false + ) + + let withReferenceEquality (com: IPythonCompiler) ctx (ent: Fable.Entity) (classMembers: Statement list) = + let isReferenceEqualityType = + (ent.IsFSharpRecord || ent.IsFSharpUnion) + && not ent.IsValueType + && not (FSharp2Fable.Util.hasStructuralEquality ent) + + if not isReferenceEqualityType then + classMembers + else + let makeMethod name args returnExpr (returnType: string) = + Statement.functionDef ( + Identifier name, + Arguments.arguments (args = Arg.arg "self" :: args), + body = [ Statement.return' returnExpr ], + returns = Expression.name returnType + ) + + [ + yield! classMembers + if not (hasClassMethod "Equals" classMembers) then + let other = + Arg.arg ("other", annotation = com.TransformImport(ctx, "Any", "typing")) + + makeMethod + "Equals" + [ other ] + (Expression.compare (thisExpr, [ Is ], [ Expression.name "other" ])) + "bool" + if not (hasClassMethod "GetHashCode" classMembers) then + makeMethod "GetHashCode" [] (libCall com ctx None "util" "physical_hash" [ thisExpr ]) "int" + ] + let ofInt (_com: IPythonCompiler) (_ctx: Context) (i: int) = Expression.intConstant (int i) let ofString (s: string) = Expression.stringConstant s diff --git a/tests/Js/Main/ComparisonTests.fs b/tests/Js/Main/ComparisonTests.fs index a2f02317a9..39ee5d9cf3 100644 --- a/tests/Js/Main/ComparisonTests.fs +++ b/tests/Js/Main/ComparisonTests.fs @@ -39,6 +39,25 @@ exception Ex of int [] type RTest2 = { a2: int; b2: int } +[] +type RefEqUnion = RefC of int | RefD of int + +[] +type NoEqRecord = { a3: int } + +[] +type NoEqUnion = NoEqCase of int + +#nowarn "386" +[] +type NoEqRecordWithOverride = + { a4: int } + override _.Equals(_) = true + override _.GetHashCode() = 4 + +[] +type NoEqStruct = { s2: int } + type Test(i: int) = member x.Value = i override x.GetHashCode() = i @@ -115,6 +134,63 @@ let genericCompare (a: 'T) (b: 'T) : int = let tests = testList "Comparison" [ + testCase "ReferenceEquality record equality works" <| fun () -> + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal true (x = x) + equal false (x = y) + equal true (x <> y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] = [ y ]) + equal false ((x, 1) = (y, 1)) + equal false (Some x = Some y) + equal true ([ x ] |> List.contains x) + equal false ([ x ] |> List.contains y) + + testCase "ReferenceEquality record hash works" <| fun () -> + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal (hash x) (hash x) + equal (genericHash x) (genericHash x) + let d = Dictionary() + d[x] <- 1 + equal true (d.ContainsKey x) + equal false (d.ContainsKey y) + equal 2 (HashSet([ x; y ]).Count) + + testCase "ReferenceEquality union equality works" <| fun () -> + let x = RefC 1 + let y = RefC 1 + equal true (x = x) + equal false (x = y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] |> List.contains y) + equal 2 (HashSet([ x; y ]).Count) + + testCase "NoEquality record and union compare by reference" <| fun () -> + let x = { a3 = 1 } + equal true ((box x).Equals(box x)) + equal false ((box x).Equals(box { a3 = 1 })) + equal true (genericEquals x x) + equal false (genericEquals x { a3 = 1 }) + let u = NoEqCase 1 + equal true ((box u).Equals(box u)) + equal false ((box u).Equals(box (NoEqCase 1))) + equal true (genericEquals u u) + equal false (genericEquals u (NoEqCase 1)) + + testCase "NoEquality record keeps an explicit Equals override" <| fun () -> + equal true ((box { a4 = 1 }).Equals(box { a4 = 2 })) + equal 4 ((box { a4 = 1 }).GetHashCode()) + + testCase "NoEquality struct record compares structurally when boxed" <| fun () -> + equal true ((box { s2 = 1 }).Equals(box { s2 = 1 })) + equal false ((box { s2 = 1 }).Equals(box { s2 = 2 })) + testCase "Typed array equality works" <| fun () -> let xs1 = [| 1; 2; 3 |] let xs2 = [| 1; 2; 3 |] diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index 675eb8b970..3db49dccc3 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -219,15 +219,15 @@ let tests = hs.Add(x3) |> equal true hs.Count |> equal 2 - // testCase "HashSet.Add with reference records works" <| fun () -> - // let x1 = { a = 5; b = 10 } - // let x2 = { a = 5; b = 10 } - // let x3 = { a = 10; b = 20 } - // let hs = HashSet() - // hs.Add(x1) |> equal true - // hs.Add(x2) |> equal true - // hs.Add(x3) |> equal true - // hs.Count |> equal 3 + testCase "HashSet.Add with reference records works" <| fun () -> + let x1 = { a = 5; b = 10 } + let x2 = { a = 5; b = 10 } + let x3 = { a = 10; b = 20 } + let hs = HashSet() + hs.Add(x1) |> equal true + hs.Add(x2) |> equal true + hs.Add(x3) |> equal true + hs.Count |> equal 3 testCase "HashSet.Clear works" <| fun () -> let hs = HashSet<_>() diff --git a/tests/Python/TestComparison.fs b/tests/Python/TestComparison.fs index d8374f46be..9e29ebcb7f 100644 --- a/tests/Python/TestComparison.fs +++ b/tests/Python/TestComparison.fs @@ -38,6 +38,34 @@ type UTest2 = exception Ex of int +[] +type RefEqRecord = { a2: int; b2: int } + +[] +type RefEqUnion = RefC of int | RefD of int + +[] +type NoEqRecord = { a3: int } + +[] +type NoEqUnion = NoEqCase of int + +#nowarn "386" +[] +type NoEqRecordWithOverride = + { a4: int } + override _.Equals(other) = true + override _.GetHashCode() = 4 + +[] +type NoEqStruct = { s2: int } + +let genericEquals (a: 'T) (b: 'T) : bool = + EqualityComparer<'T>.Default.Equals(a, b) + +let genericHash (x: 'T) : int = + EqualityComparer<'T>.Default.GetHashCode(x) + type MyTest(i: int) = member x.Value = i override x.GetHashCode() = i @@ -95,6 +123,69 @@ let ``test PhysicalEquality works`` () = // See #3998 LanguagePrimitives.PhysicalEquality r2 r2 |> equal true LanguagePrimitives.PhysicalEquality r3 r1 |> equal true +[] +let ``test ReferenceEquality record equality works`` () = + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal true (x = x) + equal false (x = y) + equal true (x <> y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] = [ y ]) + equal false ((x, 1) = (y, 1)) + equal false (Some x = Some y) + equal true ([ x ] |> List.contains x) + equal false ([ x ] |> List.contains y) + +[] +let ``test ReferenceEquality record hash works`` () = + let x = { a2 = 1; b2 = 2 } + let y = { a2 = 1; b2 = 2 } + equal (hash x) (hash x) + equal (genericHash x) (genericHash x) + let d = Dictionary() + d[x] <- 1 + equal true (d.ContainsKey x) + equal false (d.ContainsKey y) + equal 2 (HashSet([ x; y ]).Count) + +[] +let ``test ReferenceEquality union equality works`` () = + let x = RefC 1 + let y = RefC 1 + equal true (x = x) + equal false (x = y) + equal false (x.Equals y) + equal true (genericEquals x x) + equal false (genericEquals x y) + equal false ([ x ] |> List.contains y) + equal 2 (HashSet([ x; y ]).Count) + +[] +let ``test NoEquality record and union compare by reference`` () = + let x = { a3 = 1 } + equal true ((box x).Equals(box x)) + equal false ((box x).Equals(box { a3 = 1 })) + equal true (genericEquals x x) + equal false (genericEquals x { a3 = 1 }) + let u = NoEqCase 1 + equal true ((box u).Equals(box u)) + equal false ((box u).Equals(box (NoEqCase 1))) + equal true (genericEquals u u) + equal false (genericEquals u (NoEqCase 1)) + +[] +let ``test NoEquality record keeps an explicit Equals override`` () = + equal true ((box { a4 = 1 }).Equals(box { a4 = 2 })) + equal 4 ((box { a4 = 1 }).GetHashCode()) + +[] +let ``test NoEquality struct record compares structurally when boxed`` () = + equal true ((box { s2 = 1 }).Equals(box { s2 = 1 })) + equal false ((box { s2 = 1 }).Equals(box { s2 = 2 })) + [] let ``test Typed array equality works`` () = let xs1 = [| 1; 2; 3 |] diff --git a/tests/Python/TestHashSet.fs b/tests/Python/TestHashSet.fs index 4304248e50..537b02b26e 100644 --- a/tests/Python/TestHashSet.fs +++ b/tests/Python/TestHashSet.fs @@ -243,16 +243,16 @@ let ``test HashSet.Add with records works`` () = hs.Add(x3) |> equal true hs.Count |> equal 2 -// [] -// let ``test HashSet.Add with reference records works`` () = -// let x1 = { a = 5; b = 10 } -// let x2 = { a = 5; b = 10 } -// let x3 = { a = 10; b = 20 } -// let hs = HashSet() -// hs.Add(x1) |> equal true -// hs.Add(x2) |> equal true -// hs.Add(x3) |> equal true -// hs.Count |> equal 3 +[] +let ``test HashSet.Add with reference records works`` () = + let x1 = { a = 5; b = 10 } + let x2 = { a = 5; b = 10 } + let x3 = { a = 10; b = 20 } + let hs = HashSet() + hs.Add(x1) |> equal true + hs.Add(x2) |> equal true + hs.Add(x3) |> equal true + hs.Count |> equal 3 [] let ``test HashSet.Clear works`` () = From e13e7b0fdc16d26793833c77a11d242ccc61109b Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 22:19:09 +0100 Subject: [PATCH 238/322] fix(js/ts): support %o in sprintf and interpolated strings (#5175) --- src/fable-library-ts/Numeric.ts | 14 ++++++++++---- src/fable-library-ts/String.ts | 27 ++++++++++++--------------- tests/Js/Main/StringTests.fs | 24 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/fable-library-ts/Numeric.ts b/src/fable-library-ts/Numeric.ts index 50cddd6bf0..1d242ab9f2 100644 --- a/src/fable-library-ts/Numeric.ts +++ b/src/fable-library-ts/Numeric.ts @@ -109,13 +109,19 @@ export function toExponential(x: Numeric, dp?: number) { } } -export function toHex(x: Numeric) { +export function toRadix(x: Numeric, radix: number) { if (typeof x === "number") { - return (Number(x) >>> 0).toString(16); + return (Number(x) >>> 0).toString(radix); } else if (typeof x === "bigint") { // TODO: properly handle other bit sizes - return BigInt.asUintN(64, x).toString(16); - } else { + return BigInt.asUintN(64, x).toString(radix); + } else if (radix === 16) { return x[symbol]().toHex(); + } else { + return String(x); } } + +export function toHex(x: Numeric) { + return toRadix(x, 16); +} diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index d230d12e3e..eaf533c0a3 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -1,5 +1,5 @@ import { toString as dateToString } from "./Date.ts"; -import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision } from "./Numeric.ts"; +import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision, toRadix } from "./Numeric.ts"; import { escape } from "./RegExp.ts"; import { toString } from "./Types.ts"; import { Exception } from "./Util.ts"; @@ -231,17 +231,14 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, flags = flags || ""; format = format || ""; if (isNumeric(rep)) { - if (format.toLowerCase() !== "x") { - if (isLessThan(rep, 0)) { - rep = multiply(rep, -1); - sign = "-"; - } else { - if (flags.indexOf(" ") >= 0) { - sign = " "; - } else if (flags.indexOf("+") >= 0) { - sign = "+"; - } - } + const radix = format === "x" || format === "X" ? 16 : format === "o" ? 8 : 10; + if (radix === 10 && isLessThan(rep, 0)) { + rep = multiply(rep, -1); + sign = "-"; + } else if (flags.indexOf(" ") >= 0) { + sign = " "; + } else if (flags.indexOf("+") >= 0) { + sign = "+"; } precision = precision == null ? null : parseInt(precision, 10); switch (format) { @@ -255,11 +252,11 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, case "e": case "E": rep = precision != null ? toExponential(rep, precision) : toExponential(rep); break; - case "x": - rep = toHex(rep); + case "x": case "o": + rep = toRadix(rep, radix); break; case "X": - rep = toHex(rep).toUpperCase(); + rep = toRadix(rep, radix).toUpperCase(); break; default: // AOid rep = String(rep); diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 6317a7b9e5..8a5b9e4929 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -442,6 +442,30 @@ let tests = testList "Strings" [ containsInOrder ["Hello"; "Hola"] a |> equal true containsInOrder ["Hello"; "Hola"] b |> equal true + testCase "sprintf \"%o\" works" <| fun () -> + sprintf "%o" 8 |> equal "10" + sprintf "%o" 0 |> equal "0" + sprintf "%o" -1 |> equal "37777777777" + sprintf "%o" -8 |> equal "37777777770" + sprintf "%o" (1 <<< 31) |> equal "20000000000" + sprintf "%o" 255uy |> equal "377" + sprintf "%o" 65535us |> equal "177777" + sprintf "%o" 4095L |> equal "7777" + sprintf "%o" -1L |> equal "1777777777777777777777" + sprintf "%o" 18446744073709551615UL |> equal "1777777777777777777777" + + testCase "sprintf \"%o\" padding and flags work" <| fun () -> + sprintf "%5o|%-5o|%05o" 8 8 8 |> equal " 10|10 |00010" + sprintf "%+o|% o|%+05o" 8 8 8 |> equal "+10| 10|+0010" + sprintf "%+o" -1 |> equal "+37777777777" + + testCase "interpolated \"%o\" works" <| fun () -> + $"%o{-1}|%o{8}|%5o{8}" |> equal "37777777777|10| 10" + + testCase "sprintf \"%x\" sign flags work" <| fun () -> + sprintf "%+x|% x|%+X" 8 8 255 |> equal "+8| 8|+FF" + sprintf "%+x" -1 |> equal "+ffffffff" + testCase "sprintf \"%X\" works" <| fun () -> //These should all be the Native JS Versions (except int64 / uint64) //See #1530 for more information. From 04ba3853620811f6ed66eb6812bd9df3d87a1b5d Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 22:27:56 +0100 Subject: [PATCH 239/322] fix(js/ts): keep column 0 mappings in source maps (#5176) --- src/Fable.Cli/Pipeline.fs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index 638cb3b0e7..25166a2abc 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -169,12 +169,8 @@ module Js = defaultArg file sourcePath |> Path.getRelativeFileOrDirPath false targetPath false - // This is a workaround for: - // https://github.com/fable-compiler/Fable/issues/3980 - // We are still investigating why some of the F# code don't have source information - // I believe for now we can ship it like that because it only deteriorate the source map - // it should not break them completely. - if srcLine <> 0 && srcCol <> 0 && file <> Some "unknown" then + // Synthetic expressions come from FCS with line 0 and file "unknown", which SourceMapSharp rejects (#3980) + if srcLine > 0 && file <> Some "unknown" then mapGenerator.Force().AddMapping(generated, original, source = sourcePath, ?name = displayName) let compileFile (com: Compiler) (cliArgs: CliArgs) pathResolver isSilent (outPath: string) = From 5a1ab2ecb2bc91ff3e8dae1fcc0eb8a6e3a127c2 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 26 Oct 2026 22:28:01 +0100 Subject: [PATCH 240/322] test(js/ts): cover N0, C0 and P0 format specifiers (#5177) --- tests/Js/Main/ConvertTests.fs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 93dd760e84..23dad40984 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -353,6 +353,11 @@ let tests = (235672367).ToString("C2", CultureInfo.InvariantCulture) |> equal "¤235,672,367.00" + // #2582: zero precision must not leave a trailing dot + (255).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤255" + (-255).ToString("C0", CultureInfo.InvariantCulture) |> equal "(¤255)" + (235672367).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤235,672,367" + testCase "uint32.ToString 'C' works" <| fun () -> (255u).ToString("C2", CultureInfo.InvariantCulture) |> equal "¤255.00" (255u).ToString("C15", CultureInfo.InvariantCulture) |> equal "¤255.000000000000000" @@ -1027,6 +1032,11 @@ let tests = (235672367).ToString("N2", CultureInfo.InvariantCulture) |> equal "235,672,367.00" + // #2582: zero precision must not leave a trailing dot + (255).ToString("N0", CultureInfo.InvariantCulture) |> equal "255" + (-255).ToString("N0", CultureInfo.InvariantCulture) |> equal "-255" + (1000000).ToString("N0", CultureInfo.InvariantCulture) |> equal "1,000,000" + testCase "uint32.ToString 'N' works" <| fun () -> (255u).ToString("N2", CultureInfo.InvariantCulture) |> equal "255.00" @@ -1061,6 +1071,10 @@ let tests = (-255.2357).ToString("N", CultureInfo.InvariantCulture) |> equal "-255.24" + // #2582: zero precision must not leave a trailing dot + (255.7).ToString("N0", CultureInfo.InvariantCulture) |> equal "256" + (-255.7).ToString("N0", CultureInfo.InvariantCulture) |> equal "-256" + testCase "float32.ToString 'N' works" <| fun () -> (255.2357f).ToString("N2", CultureInfo.InvariantCulture) |> equal "255.24" @@ -1117,6 +1131,11 @@ let tests = (1).ToString("P2", CultureInfo.InvariantCulture) |> equal "100.00 %" (0.34).ToString("P2", CultureInfo.InvariantCulture) |> equal "34.00 %" + // #2582: zero precision must not leave a trailing dot + (1).ToString("P0", CultureInfo.InvariantCulture) |> equal "100 %" + (-1).ToString("P0", CultureInfo.InvariantCulture) |> equal "-100 %" + (0.5).ToString("P0", CultureInfo.InvariantCulture) |> equal "50 %" + testCase "uint32.ToString 'P' works" <| fun () -> (255u).ToString("P2", CultureInfo.InvariantCulture) |> equal "25,500.00 %" From b383673da61cfe3ca06416a624aaafbd732f876c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 09:50:58 +0100 Subject: [PATCH 241/322] fix(all): recompile inline users of a file with a signature (#5179) --- src/Fable.Cli/Main.fs | 2 +- src/Fable.Transforms/State.fs | 4 +- .../Compiler/Fable.Tests.Compiler.fsproj | 1 + tests/Integration/Compiler/Main.fs | 1 + .../Compiler/TestProjectWatch/.gitignore | 3 + .../TestProjectWatch/TestProjectWatch.fsproj | 19 ++++++ tests/Integration/Compiler/Util/Compiler.fs | 65 +++++++++++++++++-- .../Compiler/WatchDependencyTests.fs | 56 ++++++++++++++++ 8 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 tests/Integration/Compiler/TestProjectWatch/.gitignore create mode 100644 tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj create mode 100644 tests/Integration/Compiler/WatchDependencyTests.fs diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 10fade4b05..173eb7c806 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -1887,7 +1887,7 @@ let startCompilationAsync state = let logs = Array.append compilationResult.ErrorLogs compilationResult.OtherLogs match compilationResult.ExitCode with - | 0 -> return Ok(state, logs) + | 0 -> return Ok(compilationResult.State, logs) | _ -> return Error("Compilation failed", logs) with diff --git a/src/Fable.Transforms/State.fs b/src/Fable.Transforms/State.fs index 3d6ee95c31..60bb8832a1 100644 --- a/src/Fable.Transforms/State.fs +++ b/src/Fable.Transforms/State.fs @@ -325,12 +325,14 @@ type ImplFile = FSharp2Fable.Compiler.getRootFSharpEntities declarations |> loop entities let rootModule, rootComment = FSharp2Fable.Compiler.getRootModule declarations + let fileName = Path.normalizePathAndEnsureFsExtension file.FileName + { Declarations = declarations Entities = entities RootModule = rootModule RootComment = rootComment - InlineExprs = FSharp2Fable.Compiler.getInlineExprs file.FileName declarations + InlineExprs = FSharp2Fable.Compiler.getInlineExprs fileName declarations Signature = signature declarations } diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index a7134d6375..d2c9455820 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -30,6 +30,7 @@ + diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index 7c1909fe97..83b65a0009 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -13,6 +13,7 @@ let allTests = AnonRecordInInterface.tests CompilerHelpers.tests Inflate.tests + WatchDependency.tests ] diff --git a/tests/Integration/Compiler/TestProjectWatch/.gitignore b/tests/Integration/Compiler/TestProjectWatch/.gitignore new file mode 100644 index 0000000000..9baf46c59f --- /dev/null +++ b/tests/Integration/Compiler/TestProjectWatch/.gitignore @@ -0,0 +1,3 @@ +Library.fsi +Library.fs +Program.fs diff --git a/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj b/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj new file mode 100644 index 0000000000..7e8a49ecca --- /dev/null +++ b/tests/Integration/Compiler/TestProjectWatch/TestProjectWatch.fsproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + Major + + + + + + + + + + + + + diff --git a/tests/Integration/Compiler/Util/Compiler.fs b/tests/Integration/Compiler/Util/Compiler.fs index 70e7684c09..5880d9839e 100644 --- a/tests/Integration/Compiler/Util/Compiler.fs +++ b/tests/Integration/Compiler/Util/Compiler.fs @@ -27,7 +27,7 @@ module Compiler = /// NOTE: NOT threadsafe /// -> don't use `parallel` module Cached = - type private CachedProject(name: string) = + type private CachedProject(name: string, ?isWatch: bool, ?watchDelay: int, ?recompileAllFiles: bool) = let projDir = IO.Path.Join(__SOURCE_DIRECTORY__, "../" + name) |> Path.normalizeFullPath let projFile = IO.Path.Join(projDir, name + ".fsproj") |> Path.normalizeFullPath @@ -40,7 +40,7 @@ module Compiler = RootDir = projDir Configuration = "Debug" OutDir = None - IsWatch = false + IsWatch = defaultArg isWatch false Precompile = false PrecompiledLib = None PrintAst = false @@ -56,14 +56,33 @@ module Compiler = CompilerOptions = compilerOptions Verbosity = Verbosity.Normal } - let mutable state = State.Create(cliArgs, recompileAllFiles=true) + let state = State.Create(cliArgs, ?watchDelay = watchDelay, recompileAllFiles = defaultArg recompileAllFiles true) + let mutable watchDependencies = Map.empty member _.ProjDir = projDir - member _.Compile(files: (string * string) list) = + member _.WatchDependencies = watchDependencies + + member _.WriteFiles(files: (string * string) list) = for (fileName, content) in files do IO.File.WriteAllText(IO.Path.Join(projDir, fileName), content) + member _.ReadOutput(fileName: string) = + let path = IO.Path.Join(projDir, fileName + ".js") + if IO.File.Exists path then IO.File.ReadAllText path else "" + + /// Never returns: the watcher keeps the session alive until the process exits. + member this.StartWatching(files: (string * string) list) = + for (fileName, _) in files do + let output = IO.Path.Join(projDir, fileName + ".js") + if IO.File.Exists output then IO.File.Delete output + + this.WriteFiles files + state |> startCompilationAsync |> Async.Ignore |> Async.Start + + member this.Compile(files: (string * string) list) = + this.WriteFiles files + let result = state |> startCompilationAsync @@ -72,7 +91,7 @@ module Compiler = match result with | Error(_msg, logs) -> Array.toList logs | Ok(newState, logs) -> - state <- newState + watchDependencies <- newState.WatchDependencies Array.toList logs let private project = CachedProject("TestProject") @@ -108,6 +127,42 @@ module Compiler = "Program.fs", programFile settings source ] + let private projectWithSignatureInWatchMode = CachedProject("TestProjectSignature", isWatch = true) + + let watchDependenciesWithSignedLibrary settings (signature: string) (library: string) source = + let logs = + projectWithSignatureInWatchMode.Compile [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] + + let projDir = projectWithSignatureInWatchMode.ProjDir + + let dependencies = + projectWithSignatureInWatchMode.WatchDependencies + |> Map.toList + |> List.map (fun (file, deps) -> + IO.Path.GetRelativePath(projDir, file), deps |> Array.map (fun d -> IO.Path.GetRelativePath(projDir, d)) |> Array.toList + ) + |> Map.ofList + + logs, dependencies + + let private watchedProject = CachedProject("TestProjectWatch", isWatch = true, watchDelay = 100, recompileAllFiles = false) + + let startWatchingSignedLibrary settings (signature: string) (library: string) source = + watchedProject.StartWatching [ + "Library.fsi", signature + "Library.fs", library + "Program.fs", programFile settings source + ] + + let rewriteWatchedLibrary (library: string) = + watchedProject.WriteFiles [ "Library.fs", library ] + + let watchedOutput fileName = watchedProject.ReadOutput fileName + module Assert = open Util.Testing diff --git a/tests/Integration/Compiler/WatchDependencyTests.fs b/tests/Integration/Compiler/WatchDependencyTests.fs new file mode 100644 index 0000000000..d35d4bd846 --- /dev/null +++ b/tests/Integration/Compiler/WatchDependencyTests.fs @@ -0,0 +1,56 @@ +module Fable.Tests.Compiler.WatchDependency + +open System +open System.Threading +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private signature = + """ +module Library + +val inline triple: x: int -> int +""" + +let private library (factor: int) = + $""" +module Library + +let inline triple (x: int) = x * {factor} +""" + +let private source = + """ +let six = Library.triple 2 +""" + +let private watchDependencies signature library source = + Compiler.Cached.watchDependenciesWithSignedLibrary Compiler.Settings.standard signature library source + +let private waitForOutput (fileName: string) (expected: string) = + let deadline = DateTime.UtcNow + TimeSpan.FromSeconds 30.0 + let mutable output = Compiler.Cached.watchedOutput fileName + + while not (output.Contains expected) && DateTime.UtcNow < deadline do + Thread.Sleep 200 + output <- Compiler.Cached.watchedOutput fileName + + if not (output.Contains expected) then + failwith $"Timed out waiting for '{expected}' in {fileName}.js, last content:\n{output}" + +let tests = + testList "Watch Dependencies" [ + testCase "A file that inlines a function from a file with a signature depends on the .fs file" <| fun _ -> + let logs, deps = watchDependencies signature (library 3) source + + logs |> Assert.Is.strictSuccess |> ignore + deps |> Map.tryFind "Program.fs" |> Option.defaultValue [] |> equal [ "Library.fs" ] + + testCase "Editing an inline function in a file with a signature recompiles the files that inlined it" <| fun _ -> + Compiler.Cached.startWatchingSignedLibrary Compiler.Settings.standard signature (library 3) source + waitForOutput "Program.fs" "2 * 3" + + Compiler.Cached.rewriteWatchedLibrary (library 4) + waitForOutput "Program.fs" "2 * 4" + ] From a61ec8a1583830599a7ffb2dfb4d093e99902d4f Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 09:51:06 +0100 Subject: [PATCH 242/322] fix(all): accept RegexOptions.NonBacktracking (#5178) --- src/Fable.Transforms/Replacements.Util.fs | 1 + src/fable-library-dart/RegExp.dart | 5 +++-- src/fable-library-rust/src/RegExp.rs | 3 ++- src/fable-library-ts/RegExp.ts | 5 +++-- tests/Beam/RegexTests.fs | 8 ++++++++ tests/Dart/src/RegexTests.fs | 7 +++++++ tests/Js/Main/RegexTests.fs | 7 +++++++ tests/Python/TestRegex.fs | 8 ++++++++ tests/Rust/tests/src/RegexTests.fs | 8 ++++++++ 9 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 5f8f6f2f8d..89fbe6143d 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1293,6 +1293,7 @@ let (|RegexFlags|_|) e = | 8 -> Some [] // Compiled flag (ignored) | 16 -> Some [ RegexSingleline ] | 256 -> Some [] // ECMAScript flag (ignored) + | 1024 -> Some [] // NonBacktracking flag (ignored) | _ -> None | Operation(Binary(BinaryOrBitwise, flags1, flags2), _, _, _) -> match getFlags flags1, getFlags flags2 with diff --git a/src/fable-library-dart/RegExp.dart b/src/fable-library-dart/RegExp.dart index 80e8263eb4..5681dceb9d 100644 --- a/src/fable-library-dart/RegExp.dart +++ b/src/fable-library-dart/RegExp.dart @@ -61,9 +61,10 @@ RegExp create(String pattern, [int options = 0]) { // * Compiled: 0x0008 (ignored) // * Singleline: 0x0010 // * ECMAScript: 0x0100 (ignored) - if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256)) != 0) { + // * NonBacktracking: 0x0400 (ignored) + if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256 ^ 1024)) != 0) { throw Exception( - 'RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline and ECMAScript'); + 'RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking'); } return RegExp(pattern, unicode: true, diff --git a/src/fable-library-rust/src/RegExp.rs b/src/fable-library-rust/src/RegExp.rs index 57071f7e9b..366728c947 100644 --- a/src/fable-library-rust/src/RegExp.rs +++ b/src/fable-library-rust/src/RegExp.rs @@ -193,7 +193,8 @@ pub mod RegExp_ { // * Singleline: 0x0010 // * IgnorePatternWhitespace: 0x0020 // * ECMAScript: 0x0100 (ignored) - if ((options & !(1 | 2 | 8 | 16 | 32 | 256)) != 0) { + // * NonBacktracking: 0x0400 (ignored) + if ((options & !(1 | 2 | 8 | 16 | 32 | 256 | 1024)) != 0) { panic!("Unsupported Regex Option"); } let regex_ = regex::RegexBuilder::new(pat.as_str()) diff --git a/src/fable-library-ts/RegExp.ts b/src/fable-library-ts/RegExp.ts index de54752f71..e49b56bf51 100644 --- a/src/fable-library-ts/RegExp.ts +++ b/src/fable-library-ts/RegExp.ts @@ -9,8 +9,9 @@ export function create(pattern: string, options = 0) { // * Compiled: 0x0008 (ignored) // * Singleline: 0x0010 // * ECMAScript: 0x0100 (ignored) - if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256)) !== 0) { - throw new Exception("RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline and ECMAScript"); + // * NonBacktracking: 0x0400 (ignored) + if ((options & ~(1 ^ 2 ^ 8 ^ 16 ^ 256 ^ 1024)) !== 0) { + throw new Exception("RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking"); } // Set always global and unicode flags for compatibility with dotnet, see #2925 let flags = "gu"; diff --git a/tests/Beam/RegexTests.fs b/tests/Beam/RegexTests.fs index 15e8d85953..4b0d15432b 100644 --- a/tests/Beam/RegexTests.fs +++ b/tests/Beam/RegexTests.fs @@ -164,6 +164,14 @@ let ``test Regex.Unescape works`` () = // --- Options --- +[] +let ``test RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``test Regex.Options works`` () = let r = Regex("\\d+", RegexOptions.IgnoreCase ||| RegexOptions.Multiline) diff --git a/tests/Dart/src/RegexTests.fs b/tests/Dart/src/RegexTests.fs index 6c7020f958..acc8186619 100644 --- a/tests/Dart/src/RegexTests.fs +++ b/tests/Dart/src/RegexTests.fs @@ -41,6 +41,13 @@ let tests() = let r = Regex("[a-z]", options) int r.Options |> equal 257 + testCase "RegexOptions.NonBacktracking is accepted" <| fun _ -> + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + testCase "Regex.IsMatch with IgnoreCase and Multiline works" <| fun _ -> let str = "ab\ncd" let option1 = RegexOptions.IgnoreCase diff --git a/tests/Js/Main/RegexTests.fs b/tests/Js/Main/RegexTests.fs index 657f48b715..934d388c14 100644 --- a/tests/Js/Main/RegexTests.fs +++ b/tests/Js/Main/RegexTests.fs @@ -37,6 +37,13 @@ let tests = let r = Regex("[a-z]", options) int r.Options |> equal 257 + testCase "RegexOptions.NonBacktracking is accepted" <| fun _ -> + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + testCase "Regex.IsMatch with IgnoreCase and Multiline works" <| fun _ -> let str = "ab\ncd" let option1 = RegexOptions.IgnoreCase diff --git a/tests/Python/TestRegex.fs b/tests/Python/TestRegex.fs index de19016043..faa854f1e3 100644 --- a/tests/Python/TestRegex.fs +++ b/tests/Python/TestRegex.fs @@ -46,6 +46,14 @@ let ``test Literal regex works with whitespace characters`` () = // See #2635 Regex("\\t").IsMatch("foo\tbar") |> equal true Regex(@"\\t").IsMatch("foo\tbar") |> equal false +[] +let ``test RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``test Regex.Options works`` () = let option1 = int RegexOptions.IgnoreCase diff --git a/tests/Rust/tests/src/RegexTests.fs b/tests/Rust/tests/src/RegexTests.fs index 8ed732e583..b198b0353e 100644 --- a/tests/Rust/tests/src/RegexTests.fs +++ b/tests/Rust/tests/src/RegexTests.fs @@ -31,6 +31,14 @@ let ``Literal regex works with whitespace characters`` () = // See #2635 Regex("\\t").IsMatch("foo\tbar") |> equal true Regex(@"\\t").IsMatch("foo\tbar") |> equal false +[] +let ``RegexOptions.NonBacktracking is accepted`` () = + let options = RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase + let r = Regex("ab", options) + r.IsMatch("xABx") |> equal true + Regex.IsMatch("aaab", "(a+)*b", RegexOptions.NonBacktracking) |> equal true + Regex.Replace("a1b2", "\\d", "-", RegexOptions.NonBacktracking) |> equal "a-b-" + [] let ``Regex.Options works`` () = let option1 = RegexOptions.IgnoreCase From d332e7d42c889f172bb9501f505292abf519752f Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Tue, 27 Oct 2026 04:08:24 -0500 Subject: [PATCH 243/322] feat(js/ts/python/dart/beam): support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Beam/Replacements.fs | 26 ++++ src/Fable.Transforms/Dart/Replacements.fs | 21 +++ src/Fable.Transforms/Python/Replacements.fs | 22 +++ src/Fable.Transforms/Replacements.fs | 24 +++ src/fable-library-beam/fable_resize_array.erl | 54 +++++++ src/fable-library-dart/Array.fs | 44 ++++++ .../fable_library/resize_array.py | 54 ++++++- src/fable-library-ts/Array.fs | 44 ++++++ tests/Beam/ArrayTests.fs | 134 +++++++++++++++++ tests/Beam/ResizeArrayTests.fs | 66 ++++++++ tests/Dart/src/ArrayTests.fs | 122 +++++++++++++++ tests/Dart/src/ResizeArrayTests.fs | 68 +++++++++ tests/Js/Main/ArrayTests.fs | 127 ++++++++++++++++ tests/Js/Main/ResizeArrayTests.fs | 57 +++++++ tests/Python/TestArray.fs | 141 ++++++++++++++++++ tests/Python/TestResizeArray.fs | 62 ++++++++ 16 files changed, 1064 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 22d086f994..563878c5db 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -2144,6 +2144,26 @@ let private stringModule let private makeComparer (com: ICompiler) ctx typArg = objExpr [ "Compare", makeComparerFunction com ctx typArg ] +/// BinarySearch of an array or ResizeArray `list`, where a range search goes to `rangeFuncName`. +/// A missing comparer, or a literal null one as .NET allows, means the default comparer. +let private binarySearch (com: ICompiler) ctx r t rangeFuncName (list: Expr) (args: Expr list) = + let call funcName searchArgs (value: Expr) comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx value.Type + | Some comparer -> comparer + + Helper.LibCall(com, "fable_resize_array", funcName, t, list :: searchArgs @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binary_search" [ value ] value None + | [ value; comparer ] -> call "binary_search" [ value ] value (Some comparer) + | [ index; count; value ] -> call rangeFuncName [ index; count; value ] value None + | [ index; count; value; comparer ] -> call rangeFuncName [ index; count; value ] value (Some comparer) + | _ -> None + let private makeEqualityFunction (com: ICompiler) ctx typArg = let x = makeUniqueIdent com ctx typArg "x" let y = makeUniqueIdent com ctx typArg "y" @@ -2390,6 +2410,9 @@ let private arrays | "IndexOf", None, [ arr; value ] -> let arr = derefArr r arr Helper.LibCall(com, "fable_list", "index_of_value", t, [ value; arr ]) |> Some + // System.Array.BinarySearch, with or without a range and a comparer + | "BinarySearch", None, (ExprType(Array _) as arr) :: args -> + binarySearch com _ctx r t "binary_search_range" (derefArr r arr) args // Iterating an array as a non-generic IEnumerable (e.g. `for v in Enum.GetValues t do`). // Deref via derefArr so byte arrays (atomics, not process-dict refs) are handled too; // the non-generic System.Array of Enum.GetValues stays a ref, which get_enumerator accepts. @@ -3700,6 +3723,9 @@ let private resizeArrays ?loc = r ) |> Some + // BinarySearch + | "BinarySearch", Some callee, args -> + binarySearch com _ctx r t "binary_search_range_count" (emitExpr r (List Any) [ callee ] "get($0)") args // Remove | "Remove", Some callee, [ arg ] -> let listExpr = emitExpr r (List Any) [ callee ] "get($0)" diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 77bed3bf65..e1ebdc750b 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1696,6 +1696,25 @@ let seqModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg ) |> Some +/// BinarySearch without a comparer, or with a null one, uses the default comparer, like ResizeArray.Sort() +let binarySearch (com: ICompiler) ctx r t (i: CallInfo) rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "Array", meth, t, ar :: args @ [ comparer ], genArgs = i.GenericArgs, ?loc = r) + |> Some + + match args with + | [ value ] -> call "binarySearch" [ value ] None + | [ value; comparer ] -> call "binarySearch" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -1741,6 +1760,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.LibCall(com, "Array", "getSubArray", t, [ ar; idx; cnt ], genArgs = i.GenericArgs, ?loc = r) |> Some | "IndexOf", Some ar, args -> Helper.InstanceCall(ar, "indexOf", t, args, ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t i "binarySearchRangeCount" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insert", t, [ idx; arg ], ?loc = r) |> Some | "InsertRange", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insertAll", t, [ idx; arg ], ?loc = r) |> Some | "RemoveRange", Some ar, [ startIdx; count ] -> @@ -1823,6 +1843,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E | "IndexOf", None, args -> Helper.LibCall(com, "Array", "indexOf", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t i "binarySearchRange" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | _ -> None diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 9ed7449bd7..b3bc605756 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -1850,6 +1850,26 @@ let injectIndexOfArgs com ctx r genArgs args = injectArg com ctx r "Array" "indexOf" genArgs args +/// resize_array's binary search indexes arrays and ResizeArrays alike. +/// As in .NET, an omitted or null comparer means the default one. +let binarySearch (com: ICompiler) ctx r t rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + | None + | Some NullConst -> makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "resize_array", meth, t, ar :: args @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binary_search" [ value ] None + | [ value; comparer ] -> call "binary_search" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -1928,6 +1948,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.LibCall(com, "resize_array", "index_of", t, args @ [ ar ], ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t "binary_search_range_count" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "insert", t, [ idx; arg ], ?loc = r) |> Some | "InsertRange", Some ar, [ idx; arg ] -> Helper.LibCall(com, "resize_array", "insert_range_in_place", t, [ idx; arg; ar ], ?loc = r) @@ -2034,6 +2055,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E Helper.LibCall(com, "array", "index_of", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t "binary_search_range" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | "Resize", None, args -> let args = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index b96b3ef2f1..2852342cd7 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2155,6 +2155,28 @@ let copyToArray (com: ICompiler) r t (i: CallInfo) args = Helper.LibCall(com, "Util", "copyToArray", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some +let binarySearch (com: ICompiler) ctx r t rangeMeth (ar: Expr) args = + let call meth args comparer = + let comparer = + match comparer with + // .NET uses the default comparer when the comparer is null. The null literal + // gets here as Util.defaultOf() unless its type is nullable (see defaultof) + | None + | Some NullConst + | Some(MaybeCasted(Call(Import({ Selector = "defaultOf" }, _, _), _, _, _))) -> + makeComparer com ctx (getElementType ar.Type) + | Some comparer -> comparer + + Helper.LibCall(com, "Array", meth, t, ar :: args @ [ comparer ], ?loc = r) + |> Some + + match args with + | [ value ] -> call "binarySearch" [ value ] None + | [ value; comparer ] -> call "binarySearch" [ value ] (Some comparer) + | [ index; length; value ] -> call rangeMeth [ index; length; value ] None + | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) + | _ -> None + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -2233,6 +2255,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this | "IndexOf", Some ar, args -> let args = injectIndexOfArgs com ctx r i.GenericArgs (ar :: args) Helper.LibCall(com, "Array", "indexOf", t, args, ?loc = r) |> Some + | "BinarySearch", Some ar, args -> binarySearch com ctx r t "binarySearchRangeCount" ar args | "Insert", Some ar, [ idx; arg ] -> Helper.InstanceCall(ar, "splice", t, [ idx; makeIntConst 0; arg ], ?loc = r) |> Some @@ -2327,6 +2350,7 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E Helper.LibCall(com, "Array", "indexOf", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) |> Some + | "BinarySearch", None, ar :: args -> binarySearch com ctx r t "binarySearchRange" ar args | "GetEnumerator", Some arg, _ -> getEnumerator com r t arg |> Some | "Resize", None, args -> let args = diff --git a/src/fable-library-beam/fable_resize_array.erl b/src/fable-library-beam/fable_resize_array.erl index ec594f894c..c24a6df568 100644 --- a/src/fable-library-beam/fable_resize_array.erl +++ b/src/fable-library-beam/fable_resize_array.erl @@ -9,6 +9,9 @@ insert_range/3, get_range/3, index_of/2, index_of/3, + binary_search/3, + binary_search_range/5, + binary_search_range_count/5, find/3, find_last/2, find_all/2, @@ -32,6 +35,9 @@ -spec get_range(list(), non_neg_integer(), non_neg_integer()) -> list(). -spec index_of(list(), term()) -> integer(). -spec index_of(list(), term(), non_neg_integer()) -> integer(). +-spec binary_search(list(), term(), map() | reference()) -> integer(). +-spec binary_search_range(list(), integer(), integer(), term(), map() | reference()) -> integer(). +-spec binary_search_range_count(list(), integer(), integer(), term(), map() | reference()) -> integer(). -spec find(fun(), list(), term()) -> term(). -spec find_last(list(), fun()) -> term(). -spec find_all(list(), fun()) -> list(). @@ -117,6 +123,54 @@ index_of_impl([], _Item, _Idx) -> -1; index_of_impl([Item | _], Item, Idx) -> Idx; index_of_impl([_ | Rest], Item, Idx) -> index_of_impl(Rest, Item, Idx + 1). +%% BinarySearch with an IComparer, probing in .NET's order so duplicates and comparers get .NET's +%% answers; a miss returns the complement of the index of the first larger element. +binary_search(List, Value, Comparer) -> + Compare = fable_utils:iface_get(compare, Comparer), + binary_search_impl(list_to_tuple(List), 0, length(List) - 1, Value, Compare). + +%% System.Array.BinarySearch(array, index, length, value, comparer) +binary_search_range(List, Index, Length, Value, Comparer) -> + binary_search_range(List, Index, Length, Value, Comparer, <<"length">>). + +%% List.BinarySearch(index, count, item, comparer), whose count .NET calls "count" +binary_search_range_count(List, Index, Count, Item, Comparer) -> + binary_search_range(List, Index, Count, Item, Comparer, <<"count">>). + +%% .NET checks the range in this order before searching it +binary_search_range(_List, Index, _Count, _Value, _Comparer, _CountName) when Index < 0 -> + non_negative_number_required(<<"index">>); +binary_search_range(_List, _Index, Count, _Value, _Comparer, CountName) when Count < 0 -> + non_negative_number_required(CountName); +binary_search_range(List, Index, Count, _Value, _Comparer, _CountName) + when length(List) - Index < Count -> + erlang:error(#{ + exn_type => argument_exception, + message => + <<"Offset and length were out of bounds for the array or count is greater than the " + "number of elements from index to the end of the source collection.">> + }); +binary_search_range(List, Index, Count, Value, Comparer, _CountName) -> + Compare = fable_utils:iface_get(compare, Comparer), + binary_search_impl(list_to_tuple(List), Index, Index + Count - 1, Value, Compare). + +binary_search_impl(Tuple, Lo, Hi, Value, Compare) when Lo =< Hi -> + I = Lo + ((Hi - Lo) bsr 1), + case Compare(element(I + 1, Tuple), Value) of + 0 -> I; + C when C < 0 -> binary_search_impl(Tuple, I + 1, Hi, Value, Compare); + _ -> binary_search_impl(Tuple, Lo, I - 1, Value, Compare) + end; +binary_search_impl(_Tuple, Lo, _Hi, _Value, _Compare) -> + bnot Lo. + +non_negative_number_required(ParamName) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => + fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName) + }). + %% Find first matching predicate, return Default when not found find(_Pred, [], Default) -> Default; diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index c9fdd1480f..db4e3746e8 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -412,6 +412,50 @@ let indexOf (array: 'T[]) (item: 'T) (start: int option) (count: int option) : i | Some count when i >= start + count -> -1 | _ -> i +// Follows .NET's probe order, so duplicates and comparers with side effects give the same results +let private binarySearchFrom (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + let rec loop lo hi = + if lo > hi then + ~~~lo + else + let i = lo + ((hi - lo) >>> 1) + let c = comparer.Compare(array[i], value) + + if c = 0 then + i + elif c < 0 then + loop (i + 1) hi + else + loop lo (i - 1) + + loop index (index + length - 1) + +let private checkBinarySearchRange (lengthName: string) (array: 'T[]) (index: int) (length: int) : unit = + if index < 0 then + raise (System.ArgumentOutOfRangeException("index", "Non-negative number required.")) + + if length < 0 then + raise (System.ArgumentOutOfRangeException(lengthName, "Non-negative number required.")) + + if array.Length - index < length then + raise ( + System.ArgumentException( + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection." + ) + ) + +let binarySearch (array: 'T[]) (value: 'T) (comparer: IComparer<'T>) : int = + binarySearchFrom array 0 array.Length value comparer + +let binarySearchRange (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "length" array index length + binarySearchFrom array index length value comparer + +// ResizeArray's range overload, which names its length "count" +let binarySearchRangeCount (array: 'T[]) (index: int) (count: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "count" array index count + binarySearchFrom array index count value comparer + let partition (f: 'T -> bool) (source: 'T[]) = let res1 = ResizeArray() let res2 = ResizeArray() diff --git a/src/fable-library-py/fable_library/resize_array.py b/src/fable-library-py/fable_library/resize_array.py index e31c625c5f..d2b405f733 100644 --- a/src/fable-library-py/fable_library/resize_array.py +++ b/src/fable-library-py/fable_library/resize_array.py @@ -1,11 +1,12 @@ from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence from typing import Any, cast from .core import int32 from .option import Option, some -from .protocols import IEnumerable_1 +from .protocols import IComparer_1, IEnumerable_1 +from .system import ArgumentException__ctor_Z721C83C5, ArgumentOutOfRangeException__ctor_Z384F8060 from .util import to_iterable @@ -100,6 +101,52 @@ def index_of[T](value: T, start: int, count: int | None, xs: list[T]) -> int: return -1 +def _check_range(size: int, index: int, count: int, count_name: str) -> None: + """Raise like .NET when index and count do not describe a range of a collection of the given size.""" + if index < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("index", "Non-negative number required.") + if count < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060(count_name, "Non-negative number required.") + if size - index < count: + raise ArgumentException__ctor_Z721C83C5( + "Offset and length were out of bounds for the array or count is greater than " + + "the number of elements from index to the end of the source collection." + ) + + +def _binary_search[T](array: Sequence[T], index: int, length: int, value: T, comparer: IComparer_1[T]) -> int: + """Search array[index:index + length] in .NET's probe order. Returns ~insertion point if not found.""" + lo = index + hi = index + length - 1 + while lo <= hi: + i = lo + ((hi - lo) >> 1) + c = comparer.Compare(array[i], value) + if c == 0: + return int32(i) + if c < 0: + lo = i + 1 + else: + hi = i - 1 + return int32(~lo) + + +def binary_search[T](array: Sequence[T], value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted array or ResizeArray (System.Array.BinarySearch).""" + return _binary_search(array, 0, len(array), value, comparer) + + +def binary_search_range[T](array: Sequence[T], index: int, length: int, value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted range of an array (System.Array.BinarySearch with index and length).""" + _check_range(len(array), index, length, "length") + return _binary_search(array, index, length, value, comparer) + + +def binary_search_range_count[T](xs: list[T], index: int, count: int, value: T, comparer: IComparer_1[T]) -> int: + """Binary search a sorted range of a ResizeArray (ResizeArray.BinarySearch with index and count).""" + _check_range(len(xs), index, count, "count") + return _binary_search(xs, index, count, value, comparer) + + def insert_range_in_place[T](index: int, items: Iterable[T] | IEnumerable_1[T], xs: list[T]) -> None: """Insert a range of items into xs at the given index.""" xs[index:index] = to_iterable(items) @@ -141,6 +188,9 @@ def contains[T](value: T, xs: list[T], cons: Any | None = None) -> bool: "add_in_place", "add_range", "add_range_in_place", + "binary_search", + "binary_search_range", + "binary_search_range_count", "contains", "exists", "filter", diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index 066ecf9e29..e6832958fa 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -243,6 +243,50 @@ let indexOf<'T> let contains<'T> (value: 'T) (array: 'T[]) ([] eq: IEqualityComparer<'T>) = indexOf array value None None eq >= 0 +// Follows .NET's probe order, so duplicates and comparers with side effects give the same results +let private binarySearchFrom (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) = + let rec loop lo hi = + if lo > hi then + ~~~lo + else + let i = lo + ((hi - lo) >>> 1) + let c = comparer.Compare(array.[i], value) + + if c = 0 then + i + elif c < 0 then + loop (i + 1) hi + else + loop lo (i - 1) + + loop index (index + length - 1) + +let private checkBinarySearchRange (lengthName: string) (array: 'T[]) (index: int) (length: int) = + if index < 0 then + raise (System.ArgumentOutOfRangeException("index", "Non-negative number required.")) + + if length < 0 then + raise (System.ArgumentOutOfRangeException(lengthName, "Non-negative number required.")) + + if array.Length - index < length then + raise ( + System.ArgumentException( + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection." + ) + ) + +let binarySearch (array: 'T[]) (value: 'T) (comparer: IComparer<'T>) : int = + binarySearchFrom array 0 array.Length value comparer + +let binarySearchRange (array: 'T[]) (index: int) (length: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "length" array index length + binarySearchFrom array index length value comparer + +// ResizeArray's range overload, which names its length "count" +let binarySearchRangeCount (array: 'T[]) (index: int) (count: int) (value: 'T) (comparer: IComparer<'T>) : int = + checkBinarySearchRange "count" array index count + binarySearchFrom array index count value comparer + let empty cons = allocateArrayFromCons cons 0 let singleton value ([] cons: Cons<'T>) = diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 8fbd898d6d..36cc63eb54 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -981,6 +981,140 @@ let ``test System.Array.IndexOf works with non-primitive types`` () = System.Array.IndexOf(myArray, Dog 5) |> equal -1 System.Array.IndexOf(myArray, Duck 5) |> equal 0 +[] +let ``test System.Array.BinarySearch works`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + +[] +let ``test System.Array.BinarySearch works with duplicates`` () = + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + +[] +let ``test System.Array.BinarySearch works with a range`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + +[] +let ``test System.Array.BinarySearch works with a comparer`` () = + let desc = { new System.Collections.Generic.IComparer with member _.Compare(a, b) = compare b a } + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + +[] +let ``test System.Array.BinarySearch passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + +type BinarySearchRecord = { A: int; B: string } + +[] +let ``test System.Array.BinarySearch works with other element types`` () = + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let int64s = [|1L; 5L; 10L|] + System.Array.BinarySearch(int64s, 10L) |> equal 2 + System.Array.BinarySearch(int64s, 6L) |> equal -3 + let records = [|{ A = 1; B = "x" }; { A = 2; B = "a" }; { A = 2; B = "b" }|] + System.Array.BinarySearch(records, { A = 2; B = "b" }) |> equal 2 + System.Array.BinarySearch(records, { A = 1; B = "z" }) |> equal -2 + let unions = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(unions, Dog 0) |> equal 2 + System.Array.BinarySearch(unions, Duck 2) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a range and a null comparer`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a single element and a large array`` () = + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + +[] +let ``test System.Array.BinarySearch works with more element types`` () = + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + +[] +let ``test System.Array.BinarySearch with a range passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch with a range checks its arguments`` () = + let xs = [|1; 3; 5; 7; 9|] + let raised index length = + try + System.Array.BinarySearch(xs, index, length, 5) |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised -1 2 |> equal "ArgumentOutOfRangeException" + raised -1 -1 |> equal "ArgumentOutOfRangeException" + raised 1 -1 |> equal "ArgumentOutOfRangeException" + raised 3 3 |> equal "ArgumentException" + raised 6 0 |> equal "ArgumentException" + // --- Additional tests ported from JS --- [] diff --git a/tests/Beam/ResizeArrayTests.fs b/tests/Beam/ResizeArrayTests.fs index 77391ca97f..e8d8c3e7d8 100644 --- a/tests/Beam/ResizeArrayTests.fs +++ b/tests/Beam/ResizeArrayTests.fs @@ -65,6 +65,72 @@ let ``test ResizeArray.IndexOf works`` () = ra.IndexOf(20) |> equal 1 ra.IndexOf(99) |> equal -1 +// --- BinarySearch --- + +[] +let ``test ResizeArray.BinarySearch works`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let desc = { new IComparer with member _.Compare(a, b) = compare b a } + // Beam has no Comparer.Default, so this stands in for it + let asc = { new IComparer with member _.Compare(a, b) = compare a b } + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, asc) |> equal 3 + ra.BinarySearch(1, 2, 8, asc) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + +[] +let ``test ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer`` () = + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + +[] +let ``test ResizeArray.BinarySearch with a range passes the element to the comparer first`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + +[] +let ``test ResizeArray.BinarySearch with a range checks index and count together`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + +[] +let ``test ResizeArray.BinarySearch with a range checks its arguments`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised index count = + try + ra.BinarySearch(index, count, 3, null) |> ignore + "none" + with + | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentException -> "ArgumentException" + | _ -> "other" + raised -1 2 |> equal "ArgumentOutOfRangeException" + raised 1 -1 |> equal "ArgumentOutOfRangeException" + raised 3 3 |> equal "ArgumentException" + // --- Remove --- [] diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index a2a02efb42..b0284149ab 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -36,6 +36,16 @@ type Things = type Animal = Duck of int | Dog of int +type DescendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare y x + +type LoggingComparer(calls: ResizeArray) = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y + let tests () = testCase "Pattern matching with arrays works" <| fun () -> match [||] with [||] -> true | _ -> false @@ -440,6 +450,118 @@ let tests () = // System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 // System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 + testCase "System.Array.BinarySearch works" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + + testCase "System.Array.BinarySearch works with duplicates" <| fun () -> + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + + testCase "System.Array.BinarySearch works with a range" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + + testCase "System.Array.BinarySearch works with a comparer" <| fun () -> + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, DescendingComparer()) |> equal 3 + System.Array.BinarySearch(ys, 4, DescendingComparer()) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, DescendingComparer()) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + + testCase "System.Array.BinarySearch passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + System.Array.BinarySearch(xs, 9, LoggingComparer(calls)) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, LoggingComparer(calls)) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + + testCase "System.Array.BinarySearch works with other element types" <| fun () -> + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let int64s = [|1L; 5L; 10L|] + System.Array.BinarySearch(int64s, 10L) |> equal 2 + System.Array.BinarySearch(int64s, 6L) |> equal -3 + let things = [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(things, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(things, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let animals = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(animals, Dog 0) |> equal 2 + System.Array.BinarySearch(animals, Duck 2) |> equal -2 + + testCase "System.Array.BinarySearch works with a range and a null comparer" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + + testCase "System.Array.BinarySearch works with a single element and a large array" <| fun () -> + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + + testCase "System.Array.BinarySearch works with more element types" <| fun () -> + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + + testCase "System.Array.BinarySearch with a range passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + LoggingComparer(calls) + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + + testCase "System.Array.BinarySearch non-generic overloads work" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + + testCase "System.Array.BinarySearch with a range checks its arguments" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + testCase "Array.findIndex works" <| fun () -> let xs = [|1.f; 2.f; 3.f; 4.f|] xs |> Array.findIndex ((=) 2.f) diff --git a/tests/Dart/src/ResizeArrayTests.fs b/tests/Dart/src/ResizeArrayTests.fs index 706e5f788e..61788d980c 100644 --- a/tests/Dart/src/ResizeArrayTests.fs +++ b/tests/Dart/src/ResizeArrayTests.fs @@ -2,6 +2,20 @@ open Util +type AscendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare x y + +type DescendingComparer() = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = compare y x + +type LoggingComparer(calls: ResizeArray) = + interface System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y + let tests() = testCase "ResizeArray zero creation works" <| fun () -> let li = ResizeArray() @@ -171,6 +185,60 @@ let tests() = li.IndexOf("ab") |> equal 1 li.IndexOf("cd") |> equal -1 + testCase "ResizeArray.BinarySearch works" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, DescendingComparer()) |> equal 3 + // Comparer.Default isn't supported in Dart + ra.BinarySearch(1, 3, 7, AscendingComparer()) |> equal 3 + ra.BinarySearch(1, 2, 8, AscendingComparer()) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + + testCase "ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer" <| fun () -> + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + + testCase "ResizeArray.BinarySearch with a range passes the element to the comparer first" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + LoggingComparer(calls) + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + + testCase "ResizeArray.BinarySearch with a range checks index and count together" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + + testCase "ResizeArray.BinarySearch with a range checks its arguments" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + testCase "ResizeArray.Remove works" <| fun () -> let li = ResizeArray<_>() li.Add("ab") diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 0e97aad4f6..9f5fc8cbac 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -563,6 +563,133 @@ let tests = System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 + testCase "System.Array.BinarySearch works" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + + testCase "System.Array.BinarySearch works with duplicates" <| fun () -> + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + + testCase "System.Array.BinarySearch works with a range" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + + testCase "System.Array.BinarySearch works with a comparer" <| fun () -> + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + System.Array.BinarySearch([|1; 3; 5; 7; 9|], 7, (null: IComparer)) |> equal 3 + + testCase "System.Array.BinarySearch passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + + testCase "System.Array.BinarySearch works with other element types" <| fun () -> + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let longs = [|1L; 5L; 10L|] + System.Array.BinarySearch(longs, 10L) |> equal 2 + System.Array.BinarySearch(longs, 6L) |> equal -3 + let things = [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(things, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(things, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let animals = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(animals, Dog 0) |> equal 2 + System.Array.BinarySearch(animals, Duck 2) |> equal -2 + + testCase "System.Array.BinarySearch works with a range and a null comparer" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + + testCase "System.Array.BinarySearch works with a single element and a large array" <| fun () -> + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + + testCase "System.Array.BinarySearch works with more element types" <| fun () -> + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + + testCase "System.Array.BinarySearch with a range passes the array element to the comparer first" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + + testCase "System.Array.BinarySearch non-generic overloads work" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + + testCase "System.Array.BinarySearch with a range checks its arguments" <| fun () -> + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + testCase "Array.findIndex works" <| fun () -> let xs = [|1.f; 2.f; 3.f; 4.f|] xs |> Array.findIndex ((=) 2.f) diff --git a/tests/Js/Main/ResizeArrayTests.fs b/tests/Js/Main/ResizeArrayTests.fs index fe0b8c22d6..408699ff97 100644 --- a/tests/Js/Main/ResizeArrayTests.fs +++ b/tests/Js/Main/ResizeArrayTests.fs @@ -283,6 +283,63 @@ let tests = ns.Sort(comparer) Seq.toList ns |> equal [1;2;3] + testCase "ResizeArray.BinarySearch works" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, Comparer.Default) |> equal 3 + ra.BinarySearch(1, 2, 8, Comparer.Default) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + + testCase "ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer" <| fun () -> + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + + testCase "ResizeArray.BinarySearch with a range passes the element to the comparer first" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + + testCase "ResizeArray.BinarySearch with a range checks index and count together" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + + testCase "ResizeArray.BinarySearch with a range checks its arguments" <| fun () -> + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + testCase "ResizeArray.ToArray works" <| fun () -> let li = ResizeArray<_>() li.Add(3.); li.Add(6.); li.Add(5.); li.Add(4.); li.Add(8.) diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index 260343908c..c1f68a3c12 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -516,6 +516,147 @@ let ``test System.Array.IndexOf works with non-primitive types`` () = System.Array.IndexOf(myArray, Dog 3, 0, 1) |> equal -1 System.Array.IndexOf(myArray, Duck 5, 1) |> equal -1 +[] +let ``test System.Array.BinarySearch works`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 0) |> equal -1 + System.Array.BinarySearch(xs, 1) |> equal 0 + System.Array.BinarySearch(xs, 2) |> equal -2 + System.Array.BinarySearch(xs, 7) |> equal 3 + System.Array.BinarySearch(xs, 8) |> equal -5 + System.Array.BinarySearch(xs, 10) |> equal -6 + System.Array.BinarySearch(([||]: int[]), 3) |> equal -1 + +[] +let ``test System.Array.BinarySearch works with duplicates`` () = + System.Array.BinarySearch([|2; 2; 2; 2; 2; 2; 2; 2|], 2) |> equal 3 + System.Array.BinarySearch([|1; 2; 2; 2; 2; 3; 4; 5; 6|], 2) |> equal 4 + +[] +let ``test System.Array.BinarySearch works with a range`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2) |> equal -2 + System.Array.BinarySearch(xs, 1, 3, 9) |> equal -5 + System.Array.BinarySearch(xs, 1, 2, 9) |> equal -4 + System.Array.BinarySearch(xs, 2, 0, 5) |> equal -3 + System.Array.BinarySearch(xs, 5, 0, 5) |> equal -6 + +[] +let ``test System.Array.BinarySearch works with a comparer`` () = + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + let ys = [|9; 7; 5; 3; 1|] + System.Array.BinarySearch(ys, 3, desc) |> equal 3 + System.Array.BinarySearch(ys, 4, desc) |> equal -4 + System.Array.BinarySearch(ys, 1, 3, 3, desc) |> equal 3 + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 7, (null: System.Collections.Generic.IComparer)) |> equal 3 + +[] +let ``test System.Array.BinarySearch passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 9, logging) |> equal 4 + List.ofSeq calls |> equal [(5, 9); (7, 9); (9, 9)] + calls.Clear() + System.Array.BinarySearch(xs, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (1, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch works with other element types`` () = + let strings = [|"apple"; "banana"; "cherry"|] + System.Array.BinarySearch(strings, "banana") |> equal 1 + System.Array.BinarySearch(strings, "blue") |> equal -3 + let floats = [|1.5; 2.5; 3.5|] + System.Array.BinarySearch(floats, 2.5) |> equal 1 + System.Array.BinarySearch(floats, 3.0) |> equal -3 + let longs = [|1L; 5L; 10L|] + System.Array.BinarySearch(longs, 10L) |> equal 2 + System.Array.BinarySearch(longs, 6L) |> equal -3 + let records = + [|{ MainThing = 1; OtherThing = "x" }; { MainThing = 2; OtherThing = "a" }; { MainThing = 2; OtherThing = "b" }|] + System.Array.BinarySearch(records, { MainThing = 2; OtherThing = "b" }) |> equal 2 + System.Array.BinarySearch(records, { MainThing = 1; OtherThing = "z" }) |> equal -2 + let unions = [|Duck 1; Duck 4; Dog 0|] + System.Array.BinarySearch(unions, Dog 0) |> equal 2 + System.Array.BinarySearch(unions, Duck 2) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a range and a null comparer`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs, 1, 3, 7, null) |> equal 3 + System.Array.BinarySearch(xs, 1, 3, 2, null) |> equal -2 + +[] +let ``test System.Array.BinarySearch works with a single element and a large array`` () = + System.Array.BinarySearch([|5|], 5) |> equal 0 + System.Array.BinarySearch([|5|], 3) |> equal -1 + System.Array.BinarySearch([|5|], 7) |> equal -2 + let big = Array.init 1000 (fun i -> i * 2) + System.Array.BinarySearch(big, 998) |> equal 499 + System.Array.BinarySearch(big, 999) |> equal -501 + +[] +let ``test System.Array.BinarySearch works with more element types`` () = + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 3uy) |> equal 1 + System.Array.BinarySearch([|1uy; 3uy; 5uy|], 4uy) |> equal -3 + System.Array.BinarySearch([|-5s; 0s; 5s|], 0s) |> equal 1 + System.Array.BinarySearch([|-5s; 0s; 5s|], -6s) |> equal -1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'c') |> equal 1 + System.Array.BinarySearch([|'a'; 'c'; 'e'|], 'd') |> equal -3 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "b")) |> equal 1 + System.Array.BinarySearch([|(1, "a"); (1, "b"); (2, "a")|], (1, "c")) |> equal -3 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 2.5M) |> equal 1 + System.Array.BinarySearch([|1.5M; 2.5M; 3.5M|], 3.0M) |> equal -3 + let dates = [|System.DateTime(2020, 1, 1); System.DateTime(2021, 1, 1)|] + System.Array.BinarySearch(dates, System.DateTime(2021, 1, 1)) |> equal 1 + System.Array.BinarySearch(dates, System.DateTime(2020, 6, 1)) |> equal -2 + System.Array.BinarySearch([|1I; 5I; 10I|], 5I) |> equal 1 + System.Array.BinarySearch([|1I; 5I; 10I|], 11I) |> equal -4 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 1) |> equal 1 + System.Array.BinarySearch([|None; Some 1; Some 3|], Some 2) |> equal -3 + +[] +let ``test System.Array.BinarySearch with a range passes the array element to the comparer first`` () = + let xs = [|1; 3; 5; 7; 9|] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + System.Array.BinarySearch(xs, 1, 3, 4, logging) |> equal -3 + List.ofSeq calls |> equal [(5, 4); (3, 4)] + +[] +let ``test System.Array.BinarySearch non-generic overloads work`` () = + let xs = [|1; 3; 5; 7; 9|] + System.Array.BinarySearch(xs :> System.Array, box 7) |> equal 3 + System.Array.BinarySearch(xs :> System.Array, box 4) |> equal -3 + System.Array.BinarySearch(xs :> System.Array, 1, 3, box 5) |> equal 2 + +[] +let ``test System.Array.BinarySearch with a range checks its arguments`` () = + let xs = [|1; 3; 5; 7; 9|] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> System.Array.BinarySearch(xs, -1, 2, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, -1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 1, -1, 5)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> System.Array.BinarySearch(xs, 3, 3, 5)) |> equal "ArgumentException" + raised (fun () -> System.Array.BinarySearch(xs, 6, 0, 5)) |> equal "ArgumentException" + [] let ``test Array.findIndex works`` () = let xs = [|1.f; 2.f; 3.f; 4.f|] diff --git a/tests/Python/TestResizeArray.fs b/tests/Python/TestResizeArray.fs index dd98f6bf9d..fe07fc5cb2 100644 --- a/tests/Python/TestResizeArray.fs +++ b/tests/Python/TestResizeArray.fs @@ -193,6 +193,68 @@ let ``test ResizeArray.IndexOf works`` () = li.IndexOf("ab") |> equal 1 li.IndexOf("cd") |> equal -1 +[] +let ``test ResizeArray.BinarySearch works`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(7) |> equal 3 + ra.BinarySearch(8) |> equal -5 + let desc = ComparisonIdentity.FromFunction(fun (a: int) b -> compare b a) + (ResizeArray [9; 7; 5; 3; 1]).BinarySearch(3, desc) |> equal 3 + ra.BinarySearch(1, 3, 7, System.Collections.Generic.Comparer.Default) |> equal 3 + ra.BinarySearch(1, 2, 8, System.Collections.Generic.Comparer.Default) |> equal -4 + ra.BinarySearch(1, 2, 3, null) |> equal 1 + +[] +let ``test ResizeArray.BinarySearch works with an empty list, duplicates and a null range comparer`` () = + ResizeArray().BinarySearch(1) |> equal -1 + (ResizeArray [2; 2; 2; 2]).BinarySearch(2) |> equal 1 + (ResizeArray [1; 2; 2; 2; 2; 3]).BinarySearch(2) |> equal 2 + let ra = ResizeArray [1; 3; 5; 7; 9] + ra.BinarySearch(0, 5, 2, null) |> equal -2 + ra.BinarySearch(2, 3, 9, null) |> equal 4 + +[] +let ``test ResizeArray.BinarySearch with a range passes the element to the comparer first`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let calls = ResizeArray() + let logging = + { new System.Collections.Generic.IComparer with + member _.Compare(x, y) = + calls.Add((x, y)) + compare x y } + ra.BinarySearch(1, 3, 7, logging) |> equal 3 + List.ofSeq calls |> equal [(5, 7); (7, 7)] + +[] +let ``test ResizeArray.BinarySearch with a range checks index and count together`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(6, 0, 3, null)) |> equal "ArgumentException" + raised (fun () -> ra.BinarySearch(0, 6, 3, null)) |> equal "ArgumentException" + +[] +let ``test ResizeArray.BinarySearch with a range checks its arguments`` () = + let ra = ResizeArray [1; 3; 5; 7; 9] + let raised (f: unit -> int) = + try + f () |> ignore + "none" + with + | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentException -> "ArgumentException" + | _ -> "other" + raised (fun () -> ra.BinarySearch(-1, 2, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(1, -1, 3, null)) |> equal "ArgumentOutOfRangeException" + raised (fun () -> ra.BinarySearch(3, 3, 3, null)) |> equal "ArgumentException" + [] let ``test ResizeArray.Remove works`` () = let li = ResizeArray<_>() From 467db709de27fa8a5eb7b11f8459d1ab597fe1b9 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 10:28:33 +0100 Subject: [PATCH 244/322] ci: skip ShipIt jobs on forks (#5147) --- .github/workflows/easybuild-shipit.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/easybuild-shipit.yml b/.github/workflows/easybuild-shipit.yml index 1df8d71de9..e84c7213d1 100644 --- a/.github/workflows/easybuild-shipit.yml +++ b/.github/workflows/easybuild-shipit.yml @@ -19,8 +19,10 @@ jobs: shipit-pr: name: ShipIt - Pull Request runs-on: ubuntu-latest - # Only run if the build workflow succeeded - if: ${{ github.event.workflow_run.conclusion == 'success' }} + # Only run on the upstream repository, and only if the build workflow succeeded + if: >- + !github.event.repository.fork + && github.event.workflow_run.conclusion == 'success' steps: - uses: actions/checkout@v7.0.1 with: @@ -41,7 +43,10 @@ jobs: release: name: Release runs-on: ubuntu-latest - if: "github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ')" + if: >- + !github.event.repository.fork + && github.event.workflow_run.conclusion == 'success' + && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ') steps: - uses: actions/checkout@v7.0.1 From a8cbc5dc0c3a96639028057e132b39e559f40ced Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 11:30:04 +0100 Subject: [PATCH 245/322] feat(all): error on unsupported RegexOptions at compile time (#5180) --- src/Fable.Transforms/Beam/Replacements.fs | 14 ++- src/Fable.Transforms/Dart/Replacements.fs | 12 +++ src/Fable.Transforms/Python/Replacements.fs | 12 +++ src/Fable.Transforms/Replacements.Util.fs | 99 +++++++++++++++++-- src/Fable.Transforms/Replacements.fs | 12 +++ src/Fable.Transforms/Rust/Replacements.fs | 13 +++ src/Fable.Transforms/Transforms.Util.fs | 2 + src/fable-library-beam/fable_regex.erl | 4 + src/fable-library-py/fable_library/reg_exp.py | 4 + src/fable-library-py/tests/test_reg_exp.py | 18 ++++ .../Compiler/Fable.Tests.Compiler.fsproj | 1 + tests/Integration/Compiler/Main.fs | 1 + .../Integration/Compiler/RegexOptionsTests.fs | 56 +++++++++++ 13 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/fable-library-py/tests/test_reg_exp.py create mode 100644 tests/Integration/Compiler/RegexOptionsTests.fs diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 563878c5db..c0f298fca7 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -3516,13 +3516,25 @@ let mailbox (com: ICompiler) (_ctx: Context) r t (i: CallInfo) (thisArg: Expr op let private regex (com: ICompiler) - (_ctx: Context) + (ctx: Context) r (t: Type) (info: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + match info.CompiledName, thisArg, args with // Constructor | ".ctor", _, [ pattern ] -> Helper.LibCall(com, "fable_regex", "create", t, [ pattern ], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index e1ebdc750b..4811ae52b5 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -3309,6 +3309,18 @@ let regexMatchToSeq com t e = Helper.LibCall(com, "RegExp", "GroupIterable", t, [ e ]) let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = function | ExprType(DeclaredTypeFullName Types.regexGroup) -> true diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index b3bc605756..694e9c69c7 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3513,6 +3513,18 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let propInt p callee = getExpr r t callee (makeIntConst p) let propStr p callee = getExpr r t callee (makeStrConst p) + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = match thisArg with | Some(ExprType(DeclaredTypeFullName Types.regexGroup)) -> true diff --git a/src/Fable.Transforms/Replacements.Util.fs b/src/Fable.Transforms/Replacements.Util.fs index 89fbe6143d..2c30d27c82 100644 --- a/src/Fable.Transforms/Replacements.Util.fs +++ b/src/Fable.Transforms/Replacements.Util.fs @@ -1282,18 +1282,63 @@ let (|CustomOp|_|) (com: ICompiler) (ctx: Context) r t opName (argExprs: Expr li | Some x -> ValueSome x | None -> ValueNone +module RegexOption = + [] + let IgnoreCase = 1 + + [] + let Multiline = 2 + + [] + let ExplicitCapture = 4 + + [] + let Compiled = 8 + + [] + let Singleline = 16 + + [] + let IgnorePatternWhitespace = 32 + + [] + let RightToLeft = 64 + + [] + let ECMAScript = 256 + + [] + let CultureInvariant = 512 + + [] + let NonBacktracking = 1024 + + let names = + [ + IgnoreCase, "IgnoreCase" + Multiline, "Multiline" + ExplicitCapture, "ExplicitCapture" + Compiled, "Compiled" + Singleline, "Singleline" + IgnorePatternWhitespace, "IgnorePatternWhitespace" + RightToLeft, "RightToLeft" + ECMAScript, "ECMAScript" + CultureInvariant, "CultureInvariant" + NonBacktracking, "NonBacktracking" + ] + [] let (|RegexFlags|_|) e = let rec getFlags = function | NumberConst(NumberValue.Int32 value, _) -> match value with - | 1 -> Some [ RegexIgnoreCase ] - | 2 -> Some [ RegexMultiline ] - | 8 -> Some [] // Compiled flag (ignored) - | 16 -> Some [ RegexSingleline ] - | 256 -> Some [] // ECMAScript flag (ignored) - | 1024 -> Some [] // NonBacktracking flag (ignored) + | RegexOption.IgnoreCase -> Some [ RegexIgnoreCase ] + | RegexOption.Multiline -> Some [ RegexMultiline ] + | RegexOption.Compiled -> Some [] + | RegexOption.Singleline -> Some [ RegexSingleline ] + | RegexOption.ECMAScript -> Some [] + | RegexOption.NonBacktracking -> Some [] | _ -> None | Operation(Binary(BinaryOrBitwise, flags1, flags2), _, _, _) -> match getFlags flags1, getFlags flags2 with @@ -1305,6 +1350,48 @@ let (|RegexFlags|_|) e = | Some x -> ValueSome x | None -> ValueNone +[] +let (|RegexOptionsConst|_|) (e: Expr) = + let rec fold = + function + | NumberConst(NumberValue.Int32 value, _) -> Some value + | Operation(Binary(BinaryOrBitwise, left, right), _, _, _) -> + match fold left, fold right with + | Some left, Some right -> Some(left ||| right) + | _ -> None + | _ -> None + + match e.Type, fold e with + | Number(Int32, NumberInfo.IsEnum ent), Some value when ent.FullName = Types.regexOptions -> ValueSome value + | _ -> ValueNone + +let checkRegexOptions (com: ICompiler) (ctx: Context) r (supported: int) (args: Expr list) = + for arg in args do + match arg with + | RegexOptionsConst options when options &&& ~~~supported <> 0 -> + let unsupported = options &&& ~~~supported + + let names = + RegexOption.names + |> List.choose (fun (bit, name) -> + if unsupported &&& bit <> 0 then + Some $"RegexOptions.%s{name}" + else + None + ) + + let names = + if List.isEmpty names then + [ $"RegexOptions value 0x%X{unsupported}" ] + else + names + + let names = String.concat ", " names + + $"%s{names} is not supported by Fable on this target" + |> addError com ctx.InlinePath r + | _ -> () + [] let (|UniversalFableCoreHelpers|_|) (com: ICompiler) (ctx: Context) r t (i: CallInfo) args error = function diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 2852342cd7..2ede8569ea 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -4055,6 +4055,18 @@ let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Exp let propInt p callee = getExpr r t callee (makeIntConst p) let propStr p callee = getExpr r t callee (makeStrConst p) + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + let isGroup = match thisArg with | Some(ExprType(DeclaredTypeFullName Types.regexGroup)) -> true diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index f02f077db3..3e36ebafd7 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -2968,6 +2968,19 @@ let bclNativeImpl com ctx r t i moduleName memberName (thisArg: Expr option) (ar | None -> makeStaticMemberCall com r t i moduleName memberName args let regex com (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + checkRegexOptions + com + ctx + r + (RegexOption.IgnoreCase + ||| RegexOption.Multiline + ||| RegexOption.Compiled + ||| RegexOption.Singleline + ||| RegexOption.IgnorePatternWhitespace + ||| RegexOption.ECMAScript + ||| RegexOption.NonBacktracking) + args + match i.CompiledName with // | "GetEnumerator" -> getEnumerator com r t i thisArg.Value |> Some | meth -> diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 1572a850d8..597dc64321 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -312,6 +312,8 @@ module Types = [] let regexCaptureCollection = "System.Text.RegularExpressions.CaptureCollection" + let regexOptions = "System.Text.RegularExpressions.RegexOptions" + [] let random = "System.Random" diff --git a/src/fable-library-beam/fable_regex.erl b/src/fable-library-beam/fable_regex.erl index f51e10b7ca..e01420008a 100644 --- a/src/fable-library-beam/fable_regex.erl +++ b/src/fable-library-beam/fable_regex.erl @@ -450,6 +450,10 @@ get_options(#{options := O}) -> O. %% --- Internal helpers --- +options_to_erl(Options) when is_integer(Options), Options band bnot (1 bor 2 bor 8 bor 16 bor 256 bor 1024) =/= 0 -> + erlang:error( + <<"RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking">> + ); options_to_erl(Options) when is_integer(Options) -> Flags = [], %% IgnoreCase diff --git a/src/fable-library-py/fable_library/reg_exp.py b/src/fable-library-py/fable_library/reg_exp.py index aef8cc9f99..982731b75a 100644 --- a/src/fable-library-py/fable_library/reg_exp.py +++ b/src/fable-library-py/fable_library/reg_exp.py @@ -58,6 +58,10 @@ def GetEnumerator(self, __unit: Unit = UNIT) -> IEnumerator[Match[str]]: def _options_to_flags(options: int) -> int: """Convert .NET options to Python flags.""" + if options & ~(1 | 2 | 8 | 16 | 256 | 1024): + raise Exception( + "RegexOptions only supports: IgnoreCase, Multiline, Compiled, Singleline, ECMAScript and NonBacktracking" + ) flags = 256 if options & 1: flags |= re.IGNORECASE diff --git a/src/fable-library-py/tests/test_reg_exp.py b/src/fable-library-py/tests/test_reg_exp.py new file mode 100644 index 0000000000..0f15d2e01b --- /dev/null +++ b/src/fable-library-py/tests/test_reg_exp.py @@ -0,0 +1,18 @@ +import pytest +from fable_library.reg_exp import create + + +def test_accepted_options_compile() -> None: + ignore_case = 1 + compiled = 8 + non_backtracking = 1024 + regex = create("ab", ignore_case | compiled | non_backtracking) + + assert regex.search("xABx") is not None + + +def test_unsupported_option_raises() -> None: + right_to_left = 64 + + with pytest.raises(Exception, match="RegexOptions only supports"): + create("ab", right_to_left) diff --git a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj index d2c9455820..ab4f65a052 100644 --- a/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj +++ b/tests/Integration/Compiler/Fable.Tests.Compiler.fsproj @@ -31,6 +31,7 @@ + diff --git a/tests/Integration/Compiler/Main.fs b/tests/Integration/Compiler/Main.fs index 83b65a0009..86f7f71ef4 100644 --- a/tests/Integration/Compiler/Main.fs +++ b/tests/Integration/Compiler/Main.fs @@ -14,6 +14,7 @@ let allTests = CompilerHelpers.tests Inflate.tests WatchDependency.tests + RegexOptions.tests ] diff --git a/tests/Integration/Compiler/RegexOptionsTests.fs b/tests/Integration/Compiler/RegexOptionsTests.fs new file mode 100644 index 0000000000..0924e9fb36 --- /dev/null +++ b/tests/Integration/Compiler/RegexOptionsTests.fs @@ -0,0 +1,56 @@ +module Fable.Tests.Compiler.RegexOptions + +open Util.Testing +open Fable.Tests.Compiler.Util +open Fable.Tests.Compiler.Util.Compiler + +let private compile source = Compiler.Cached.compile Compiler.Settings.standard source + +let tests = + testList "Regex Options" [ + testCase "An unsupported option on the constructor is a compile error" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let r = Regex("a", RegexOptions.RightToLeft) +""" + compile source + |> Assert.Is.error + |> Assert.Exists.errorWith "RegexOptions.RightToLeft is not supported" + |> ignore + + testCase "Only the unsupported options of a combination are reported" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let found = Regex.IsMatch("abc", "b", RegexOptions.IgnoreCase ||| RegexOptions.ExplicitCapture ||| RegexOptions.CultureInvariant) +""" + compile source + |> Assert.Exists.errorWith "RegexOptions.ExplicitCapture, RegexOptions.CultureInvariant is not supported" + |> ignore + + testCase "Options that only affect performance compile without a diagnostic" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let r = Regex("a", RegexOptions.Compiled ||| RegexOptions.NonBacktracking ||| RegexOptions.IgnoreCase) +let parts = Regex.Split("a1b", "\\d", RegexOptions.ECMAScript) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Options that are only known at run time are left to the runtime check" <| fun _ -> + let source = + """ +open System.Text.RegularExpressions + +let make (options: RegexOptions) = Regex("a", options) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + ] From 2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3 Mon Sep 17 00:00:00 2001 From: cabboose <57953499+shayanhabibi@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:30:32 +0800 Subject: [PATCH 246/322] perf(js/ts): standard library one pass List.append builder (#5148) Co-authored-by: Claude Opus 5.5 Co-authored-by: Mangel Maxime --- src/fable-library-ts/List.fs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index bf29b18fa2..18601aa941 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -394,8 +394,21 @@ let scan (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T list) = let scanBack (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = Array.scanBack folder (toArray xs) state |> ofArray +// Valid on a non-empty list: a reference-type option is erased, so tail holds the node; the cast is for tsc. +[] +let inline private uncheckedTail (xs: 'T list) : 'T list = nativeOnly + let append (xs: 'T list) (ys: 'T list) = - fold (fun acc x -> List.Cons(x, acc)) ys (reverse xs) + let root = List.Empty + let mutable node = root + let mutable rest = xs + + while rest.tail.IsSome do + node <- node.AppendConsNoTail rest.head + rest <- uncheckedTail rest + + node.SetConsTail ys + root.Tail let collect (mapping: 'T -> 'U list) (xs: 'T list) = let root = List.Empty From 65cd419d78e4dbc2008f8a707d5298e390defbee Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 11:59:33 +0100 Subject: [PATCH 247/322] perf(js/ts): cast erased option unwraps in TypeScript output (#5181) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 5 ++++- src/Fable.Transforms/Transforms.Util.fs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index c76713ca33..d312bbdf57 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -2663,8 +2663,11 @@ but thanks to the optimisation done below we get | Fable.OptionValue -> let expr = com.TransformAsExpr(ctx, fableExpr) - if mustWrapOption typ || com.IsTypeScript then + // Functions are stored uncurried, so their annotation would not match the value + if mustWrapOption typ || (com.IsTypeScript && containsFunctionType typ) then libCall com ctx None "Option" "value" [] [ expr ] + elif com.IsTypeScript then + AsExpression(expr, makeTypeAnnotation com ctx typ) else expr diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 597dc64321..73d06ab41b 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -1032,6 +1032,12 @@ module AST = | Option _ -> true | _ -> false + let rec containsFunctionType (t: Type) = + match t with + | LambdaType _ + | DelegateType _ -> true + | t -> t.Generics |> List.exists containsFunctionType + let isUnitOfMeasure t = match t with | Measure _ From bb0faa794080a04e7791d419b2fa4d2ad107c186 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 14:16:49 +0100 Subject: [PATCH 248/322] fix(js/ts/dart): float to small integer and char conversions (#5182) --- src/Fable.Transforms/Dart/Replacements.fs | 1 + src/Fable.Transforms/Replacements.fs | 9 +++++---- tests/Js/Main/ConvertTests.fs | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 4811ae52b5..062403f731 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -232,6 +232,7 @@ let toChar com (ctx: Context) r (arg: Expr) = // TODO: Check length | Char -> arg | String -> Helper.InstanceCall(arg, "codeUnitAt", Char, [ makeIntConst 0 ]) + | Number((Float16 | Float32 | Float64), _) -> Helper.InstanceCall(arg, "toInt", Char, []) | _ -> TypeCast(arg, Char) let charToString = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 2ede8569ea..0c696080db 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -352,11 +352,12 @@ let toLong com (ctx: Context) r targetType (args: Expr list) : Expr = let emitIntCast toKind arg = match toKind with - | Int8 -> emitExpr None Int8.Number [ arg ] "($0 + 0x80 & 0xFF) - 0x80" - | Int16 -> emitExpr None Int16.Number [ arg ] "($0 + 0x8000 & 0xFFFF) - 0x8000" + // `~~` truncates toward zero before the wrap, so negative fractions round like .NET + | Int8 -> emitExpr None Int8.Number [ arg ] "(~~$0 + 0x80 & 0xFF) - 0x80" + | Int16 -> emitExpr None Int16.Number [ arg ] "(~~$0 + 0x8000 & 0xFFFF) - 0x8000" | Int32 -> fastIntFloor arg - | UInt8 -> emitExpr None UInt8.Number [ arg ] "$0 & 0xFF" - | UInt16 -> emitExpr None UInt16.Number [ arg ] "$0 & 0xFFFF" + | UInt8 -> emitExpr None UInt8.Number [ arg ] "~~$0 & 0xFF" + | UInt16 -> emitExpr None UInt16.Number [ arg ] "~~$0 & 0xFFFF" | UInt32 -> emitExpr None UInt32.Number [ arg ] "$0 >>> 0" | _ -> FableError $"Unexpected non-integer type %A{toKind}" |> raise diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 23dad40984..7cd82bdbb9 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -1286,6 +1286,14 @@ let tests = Convert.ToChar("a") |> equal x Convert.ToChar('a') |> equal x + testCase "Negative fractions truncate toward zero when converted to small integers" <| fun () -> + sbyte(-7.9) |> equal -7y + sbyte(-7.9f) |> equal -7y + int16(-7.9) |> equal -7s + int16(-7.9f) |> equal -7s + byte(7.9) |> equal 7uy + uint16(7.9) |> equal 7us + testCase "System.Convert.ToSByte works" <| fun () -> let x = 1y sbyte(1y) |> equal x From 6efa86d35c5943dc8bbb6da0f27cc37095276064 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 14:17:08 +0100 Subject: [PATCH 249/322] feat(js/ts/python/dart): support Nullable conversions (#5183) --- src/Fable.Transforms/Dart/Replacements.fs | 38 +++++ src/Fable.Transforms/Python/Replacements.fs | 40 +++++ src/Fable.Transforms/Replacements.fs | 30 ++++ tests/Dart/main.dart | 2 + tests/Dart/src/Fable.Tests.Dart.fsproj | 1 + tests/Dart/src/NullableTests.fs | 60 +++++++ tests/Js/Main/Fable.Tests.fsproj | 1 + tests/Js/Main/Main.fs | 1 + tests/Js/Main/NullableTests.fs | 170 ++++++++++++++++++++ tests/Python/Fable.Tests.Python.fsproj | 1 + tests/Python/TestNullable.fs | 72 +++++++++ 11 files changed, 416 insertions(+) create mode 100644 tests/Dart/src/NullableTests.fs create mode 100644 tests/Js/Main/NullableTests.fs create mode 100644 tests/Python/TestNullable.fs diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 062403f731..66b24c4b58 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2055,6 +2055,43 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> makeEqOp r c (makeNull ()) BinaryUnequal |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number((Float16 | Float32 | Float64), _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | Number _ -> toInt com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> makeNull () |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx value.Type "v" + + Let( + v, + value, + IfThenElse( + makeEqOp None (IdentExpr v) (makeNull ()) BinaryUnequal, + TypeCast(convert targetType (TypeCast(IdentExpr v, sourceType)), t), + makeNull (), + r + ) + ) + |> Some + | _ -> None + let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with | "Some", _ -> NewOption(List.tryHead args, t.Generics.Head, isStruct) |> makeValue r |> Some @@ -4022,6 +4059,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 694e9c69c7..3da6ef0dfd 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2247,6 +2247,45 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number(UInt64, _) -> toLong com ctx r true targetType [ value ] + | Number(Int64, _) -> toLong com ctx r false targetType [ value ] + | Number((Float16 | Float32 | Float64), _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | Number _ -> toInt com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> Value(Null t, None) |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx value.Type "v" + + Let( + v, + value, + IfThenElse( + Test(IdentExpr v, OptionTest true, None), + TypeCast(convert targetType (TypeCast(IdentExpr v, sourceType)), t), + Value(Null t, None), + r + ) + ) + |> Some + | _ -> None + // See fable-library-ts/Option.ts for more info on how options behave in Fable runtime let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with @@ -4193,6 +4232,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 0c696080db..cf7eccd048 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2563,6 +2563,35 @@ let nullables (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: | "get_HasValue", Some c -> Test(c, OptionTest true, r) |> Some | _ -> None +let nullableModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = + let convert (targetType: Type) (value: Expr) = + match targetType with + | Char -> toChar com ctx r value + | Number(BigIntegers _, _) -> toLong com ctx r targetType [ value ] + | Number(Integers _, _) -> toInt com ctx r targetType [ value ] + | Number(Floats _, _) -> toFloat com ctx r targetType [ value ] + | Number(Decimal, _) -> toDecimal com ctx r targetType [ value ] + | _ -> TypeCast(value, targetType) + + match i.CompiledName.Replace("$W", ""), List.tryLast args, t with + | ("ToByte" | "ToUInt8" | "ToSByte" | "ToInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToInt32" | "ToUInt" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr" | "ToSingle" | "ToFloat32" | "ToDouble" | "ToFloat" | "ToDecimal" | "ToChar" | "ToEnum"), + Some value, + Nullable(targetType, _) -> + match value with + | Value(Null _, _) -> Value(Null t, None) |> Some + | Value((NumberConstant _ | CharConstant _), _) -> convert targetType value |> Some + | _ -> + let sourceType = + match value.Type with + | Nullable(sourceType, _) -> sourceType + | sourceType -> sourceType + + let v = makeUniqueIdent com ctx sourceType "v" + let mapping = Delegate([ v ], convert targetType (IdentExpr v), None, Tags.empty) + + Helper.LibCall(com, "Option", "map", t, [ mapping; value ], ?loc = r) |> Some + | _ -> None + // See fable-library-ts/Option.ts for more info on how options behave in Fable runtime let options isStruct (com: ICompiler) (_: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg with @@ -4839,6 +4868,7 @@ let private replacedModules = "System.Math", operators "System.MathF", operators "Microsoft.FSharp.Core.Operators", operators + "Microsoft.FSharp.Linq.NullableModule", nullableModule "Microsoft.FSharp.Core.Operators.Checked", operators "Microsoft.FSharp.Core.Operators.Unchecked", unchecked "Microsoft.FSharp.Core.Operators.OperatorIntrinsics", intrinsicFunctions diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart index 9149d8cba7..3a2855f4a6 100644 --- a/tests/Dart/main.dart +++ b/tests/Dart/main.dart @@ -14,6 +14,7 @@ import './src/ListTests.dart' as list; import './src/MapTests.dart' as map; import './src/MiscTests.dart' as misc; // import './src/NullnessTests.dart' as nullness; +import './src/NullableTests.dart' as nullable; import './src/OptionTests.dart' as option; import './src/QuotationTests.dart' as quotation; import './src/RecordTests.dart' as record; @@ -49,6 +50,7 @@ void main() { map.tests(); misc.tests(); // nullness.tests(); + nullable.tests(); option.tests(); quotation.tests(); record.tests(); diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index 4cbb0780d8..47da2d9ccf 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -24,6 +24,7 @@ + diff --git a/tests/Dart/src/NullableTests.fs b/tests/Dart/src/NullableTests.fs new file mode 100644 index 0000000000..1a8b13689b --- /dev/null +++ b/tests/Dart/src/NullableTests.fs @@ -0,0 +1,60 @@ +module Fable.Tests.Dart.Nullable + +open System +open Util +open FSharp.Linq + +let private none<'T when 'T: struct and 'T: (new: unit -> 'T) and 'T :> ValueType> = Nullable<'T>() + +let tests() = + testCase "Nullable.char works" <| fun () -> + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char none |> equal none + + testCase "Nullable.enum works" <| fun () -> + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum none: Nullable) |> equal none + + testCase "Nullable.float and Nullable.float32 work" <| fun () -> + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float none |> equal none + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 none |> equal none + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + + testCase "Nullable integer conversions work" <| fun () -> + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int none |> equal none + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 none |> equal none + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint64 none |> equal none + + testCase "Nullable conversions evaluate their argument once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index ce2d68b963..1fbca90fc9 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -68,6 +68,7 @@ + diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index f42d889826..bcf67edab3 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -34,6 +34,7 @@ let allTests = Maps.tests Misc.tests Nullness.tests + Nullable.tests NestedAndRecursivePatternTests.tests Observable.tests Option.tests diff --git a/tests/Js/Main/NullableTests.fs b/tests/Js/Main/NullableTests.fs new file mode 100644 index 0000000000..553e519989 --- /dev/null +++ b/tests/Js/Main/NullableTests.fs @@ -0,0 +1,170 @@ +module Fable.Tests.Nullable + +open Util.Testing +open System +open FSharp.Linq + +let private none<'T when 'T: struct and 'T: (new: unit -> 'T) and 'T :> ValueType> = Nullable<'T>() + +let tests = + testList "Nullable" [ + testCase "Nullable.char" <| fun () -> + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 50y) |> equal (Nullable '2') + Nullable.char (Nullable 51s) |> equal (Nullable '3') + Nullable.char (Nullable 52us) |> equal (Nullable '4') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 54u) |> equal (Nullable '6') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 56UL) |> equal (Nullable '8') + Nullable.char (Nullable 57m) |> equal (Nullable '9') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 59.0f) |> equal (Nullable ';') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char none |> equal none + + testCase "Nullable.decimal" <| fun () -> + Nullable.decimal (Nullable 1uy) |> equal (Nullable 1m) + Nullable.decimal (Nullable 2y) |> equal (Nullable 2m) + Nullable.decimal (Nullable 3s) |> equal (Nullable 3m) + Nullable.decimal (Nullable 4us) |> equal (Nullable 4m) + Nullable.decimal (Nullable 5) |> equal (Nullable 5m) + Nullable.decimal (Nullable 6u) |> equal (Nullable 6m) + Nullable.decimal (Nullable 7L) |> equal (Nullable 7m) + Nullable.decimal (Nullable 8UL) |> equal (Nullable 8m) + Nullable.decimal (Nullable 9m) |> equal (Nullable 9m) + Nullable.decimal (Nullable 10.5) |> equal (Nullable 10.5m) + Nullable.decimal (Nullable 11.5f) |> equal (Nullable 11.5m) + Nullable.decimal none |> equal none + + testCase "Nullable.double" <| fun () -> + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.double (Nullable 7L) |> equal (Nullable 7.0) + Nullable.double (Nullable 9m) |> equal (Nullable 9.0) + Nullable.double (Nullable 11.5f) |> equal (Nullable 11.5) + Nullable.double none |> equal none + + testCase "Nullable.enum" <| fun () -> + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum none: Nullable) |> equal none + + testCase "Nullable.float" <| fun () -> + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 2y) |> equal (Nullable 2.0) + Nullable.float (Nullable 3s) |> equal (Nullable 3.0) + Nullable.float (Nullable 4us) |> equal (Nullable 4.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 6u) |> equal (Nullable 6.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 8UL) |> equal (Nullable 8.0) + Nullable.float (Nullable 9m) |> equal (Nullable 9.0) + Nullable.float (Nullable 10.0) |> equal (Nullable 10.0) + Nullable.float (Nullable 11.0f) |> equal (Nullable 11.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float none |> equal none + + testCase "Nullable.float32" <| fun () -> + Nullable.float32 (Nullable 1uy) |> equal (Nullable 1.0f) + Nullable.float32 (Nullable 2y) |> equal (Nullable 2.0f) + Nullable.float32 (Nullable 3s) |> equal (Nullable 3.0f) + Nullable.float32 (Nullable 4us) |> equal (Nullable 4.0f) + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 6u) |> equal (Nullable 6.0f) + Nullable.float32 (Nullable 7L) |> equal (Nullable 7.0f) + Nullable.float32 (Nullable 8UL) |> equal (Nullable 8.0f) + Nullable.float32 (Nullable 9m) |> equal (Nullable 9.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 (Nullable 11.0f) |> equal (Nullable 11.0f) + Nullable.float32 (Nullable 'c') |> equal (Nullable 99.0f) + Nullable.float32 none |> equal none + + testCase "Nullable.int" <| fun () -> + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 2y) |> equal (Nullable 2) + Nullable.int (Nullable 3s) |> equal (Nullable 3) + Nullable.int (Nullable 4us) |> equal (Nullable 4) + Nullable.int (Nullable 5) |> equal (Nullable 5) + Nullable.int (Nullable 6u) |> equal (Nullable 6) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 8UL) |> equal (Nullable 8) + Nullable.int (Nullable 9m) |> equal (Nullable 9) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9f) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int none |> equal none + + testCase "Nullable.int16" <| fun () -> + Nullable.int16 (Nullable 5) |> equal (Nullable 5s) + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int16 (Nullable 9.9) |> equal (Nullable 9s) + Nullable.int16 none |> equal none + + testCase "Nullable.int32" <| fun () -> + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int32 (Nullable -7.9) |> equal (Nullable -7) + Nullable.int32 (Nullable 9m) |> equal (Nullable 9) + Nullable.int32 none |> equal none + + testCase "Nullable.int64" <| fun () -> + Nullable.int64 (Nullable 5) |> equal (Nullable 5L) + Nullable.int64 (Nullable 7UL) |> equal (Nullable 7L) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 (Nullable 11m) |> equal (Nullable 11L) + Nullable.int64 none |> equal none + + testCase "Nullable.int8" <| fun () -> + Nullable.int8 (Nullable 5) |> equal (Nullable 5y) + Nullable.int8 (Nullable -7L) |> equal (Nullable -7y) + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.int8 none |> equal none + + testCase "Nullable.sbyte" <| fun () -> + Nullable.sbyte (Nullable 5) |> equal (Nullable 5y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.sbyte none |> equal none + + testCase "Nullable.single" <| fun () -> + Nullable.single (Nullable 5) |> equal (Nullable 5.0f) + Nullable.single (Nullable 7L) |> equal (Nullable 7.0f) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + Nullable.single none |> equal none + + testCase "Nullable.uint" <| fun () -> + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint (Nullable 9.9) |> equal (Nullable 9u) + Nullable.uint none |> equal none + + testCase "Nullable.uint16" <| fun () -> + Nullable.uint16 (Nullable 5) |> equal (Nullable 5us) + Nullable.uint16 (Nullable 7L) |> equal (Nullable 7us) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint16 none |> equal none + + testCase "Nullable.uint32" <| fun () -> + Nullable.uint32 (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint32 (Nullable 9.9) |> equal (Nullable 9u) + Nullable.uint32 none |> equal none + + testCase "Nullable.uint64" <| fun () -> + Nullable.uint64 (Nullable 5) |> equal (Nullable 5UL) + Nullable.uint64 (Nullable 7L) |> equal (Nullable 7UL) + Nullable.uint64 (Nullable 9.9) |> equal (Nullable 9UL) + Nullable.uint64 (Nullable 11m) |> equal (Nullable 11UL) + Nullable.uint64 none |> equal none + + testCase "Nullable.byte and Nullable.uint8" <| fun () -> + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint8 (Nullable 9.9) |> equal (Nullable 9uy) + Nullable.byte none |> equal none + + testCase "Nullable conversions evaluate their argument once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 + ] diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index b5f130a704..1feeccc0dd 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -64,6 +64,7 @@ + diff --git a/tests/Python/TestNullable.fs b/tests/Python/TestNullable.fs new file mode 100644 index 0000000000..0370bddd71 --- /dev/null +++ b/tests/Python/TestNullable.fs @@ -0,0 +1,72 @@ +module Fable.Tests.Nullable + +open System +open Util.Testing +open FSharp.Linq + +[] +let ``test Nullable.char works`` () = + Nullable.char (Nullable 49uy) |> equal (Nullable '1') + Nullable.char (Nullable 53) |> equal (Nullable '5') + Nullable.char (Nullable 55L) |> equal (Nullable '7') + Nullable.char (Nullable 57m) |> equal (Nullable '9') + Nullable.char (Nullable 58.0) |> equal (Nullable ':') + Nullable.char (Nullable 'a') |> equal (Nullable 'a') + Nullable.char (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable.decimal works`` () = + Nullable.decimal (Nullable 5) |> equal (Nullable 5m) + Nullable.decimal (Nullable 7L) |> equal (Nullable 7m) + Nullable.decimal (Nullable 10.5) |> equal (Nullable 10.5m) + Nullable.decimal (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable.enum works`` () = + (Nullable.enum (Nullable 2): Nullable) |> equal (Nullable DayOfWeek.Tuesday) + (Nullable.enum (Nullable()): Nullable) |> equal (Nullable()) + +[] +let ``test Nullable.float and Nullable.float32 work`` () = + Nullable.float (Nullable 1uy) |> equal (Nullable 1.0) + Nullable.float (Nullable 5) |> equal (Nullable 5.0) + Nullable.float (Nullable 7L) |> equal (Nullable 7.0) + Nullable.float (Nullable 9m) |> equal (Nullable 9.0) + Nullable.float (Nullable 'c') |> equal (Nullable 99.0) + Nullable.float (Nullable()) |> equal (Nullable()) + Nullable.float32 (Nullable 5) |> equal (Nullable 5.0f) + Nullable.float32 (Nullable 10.0) |> equal (Nullable 10.0f) + Nullable.float32 (Nullable()) |> equal (Nullable()) + Nullable.double (Nullable 5) |> equal (Nullable 5.0) + Nullable.single (Nullable 9.5) |> equal (Nullable 9.5f) + +[] +let ``test Nullable integer conversions work`` () = + Nullable.int (Nullable 1uy) |> equal (Nullable 1) + Nullable.int (Nullable 7L) |> equal (Nullable 7) + Nullable.int (Nullable 9m) |> equal (Nullable 9) + Nullable.int (Nullable 10.9) |> equal (Nullable 10) + Nullable.int (Nullable -11.9) |> equal (Nullable -11) + Nullable.int (Nullable 'c') |> equal (Nullable 99) + Nullable.int (Nullable()) |> equal (Nullable()) + Nullable.int16 (Nullable -7L) |> equal (Nullable -7s) + Nullable.int32 (Nullable 5L) |> equal (Nullable 5) + Nullable.int64 (Nullable -9.9) |> equal (Nullable -9L) + Nullable.int64 (Nullable()) |> equal (Nullable()) + Nullable.int8 (Nullable 9.9) |> equal (Nullable 9y) + Nullable.sbyte (Nullable -7.9) |> equal (Nullable -7y) + Nullable.byte (Nullable 5) |> equal (Nullable 5uy) + Nullable.uint8 (Nullable 7L) |> equal (Nullable 7uy) + Nullable.uint16 (Nullable 9.9) |> equal (Nullable 9us) + Nullable.uint (Nullable 5) |> equal (Nullable 5u) + Nullable.uint32 (Nullable 7L) |> equal (Nullable 7u) + Nullable.uint64 (Nullable()) |> equal (Nullable()) + +[] +let ``test Nullable conversions evaluate their argument once`` () = + let mutable calls = 0 + let next () = + calls <- calls + 1 + Nullable 5 + Nullable.float (next ()) |> equal (Nullable 5.0) + calls |> equal 1 From 187f89852413186b52d4ecdde55e1be7cf736785 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 14:46:23 +0100 Subject: [PATCH 250/322] perf(js/ts): list node as a class with a shared empty node (#5185) --- src/fable-library-ts/List.fs | 134 +++++++++++++++++------------------ 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index 18601aa941..3cf4d74945 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -3,31 +3,21 @@ module ListModule open Fable.Core open Native -[] [] type LinkedList<'T> = - { - head: 'T - mutable tail: LinkedList<'T> option - } + val head: 'T + val mutable tail: LinkedList<'T> option - static member Empty: 'T list = + new(head: 'T, tail: LinkedList<'T> option) + = { - head = Unchecked.defaultof<'T> - tail = None + head = head + tail = tail } - static member Cons(x: 'T, xs: 'T list) = - { - head = x - tail = Some xs - } + static member inline Cons(x: 'T, xs: 'T list) = LinkedList<'T>(x, Some xs) - static member inline internal ConsNoTail(x: 'T) = - { - head = x - tail = None - } + static member inline internal ConsNoTail(x: 'T) = LinkedList<'T>(x, None) member inline internal xs.SetConsTail(t: 'T list) = xs.tail <- Some t @@ -169,6 +159,16 @@ and ListEnumerator<'T>(xs: 'T list) = and 'T list = LinkedList<'T> and List<'T> = LinkedList<'T> +// F# has no generic values, so the shared node is typed obj and cast per use +let private emptyNode: LinkedList = + LinkedList(Unchecked.defaultof, None) + +let empty<'T> () : 'T list = unbox<'T list> emptyNode + +// Builders mutate the tail of their root node, so it must not be the shared empty node +let inline private newRoot<'T> () : 'T list = + List.ConsNoTail(Unchecked.defaultof<'T>) + // [] // [] // module List = @@ -176,11 +176,9 @@ and List<'T> = LinkedList<'T> let indexNotFound () = raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) -let empty () = List.Empty - let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) -let singleton x = List.Cons(x, List.Empty) +let singleton x = List.Cons(x, empty ()) let isEmpty (xs: 'T list) = xs.IsEmpty @@ -261,7 +259,7 @@ let fold<'T, 'State> (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T li acc let reverse (xs: 'T list) = - fold (fun acc x -> List.Cons(x, acc)) List.Empty xs + fold (fun acc x -> List.Cons(x, acc)) (empty ()) xs let foldBack<'T, 'State> (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = // fold (fun acc x -> folder x acc) state (reverse xs) @@ -302,9 +300,9 @@ let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = | None -> node | Some(x, acc) -> loop acc (node.AppendConsNoTail x) - let root = List.Empty + let root = newRoot () let node = loop state root - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let iterate action xs = fold (fun () x -> action x) () xs @@ -344,24 +342,24 @@ let ofArrayWithTail (xs: 'T[]) (tail: 'T list) = res -let ofArray (xs: 'T[]) = ofArrayWithTail xs List.Empty +let ofArray (xs: 'T[]) = ofArrayWithTail xs (empty ()) let ofSeq (xs: seq<'T>) : 'T list = match xs with | :? array<'T> as xs -> ofArray xs | :? list<'T> as xs -> xs | _ -> - let root = List.Empty + let root = newRoot () let mutable node = root for x in xs do node <- node.AppendConsNoTail x - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let concat (lists: seq<'T list>) = - let root = List.Empty + let root = newRoot () let mutable node = root let action xs = @@ -374,11 +372,11 @@ let concat (lists: seq<'T list>) = for xs in lists do action xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let scan (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let mutable node = root.AppendConsNoTail state let mutable acc = state let mutable xs = xs @@ -388,7 +386,7 @@ let scan (folder: 'State -> 'T -> 'State) (state: 'State) (xs: 'T list) = node <- node.AppendConsNoTail acc xs <- xs.Tail - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let scanBack (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = @@ -399,7 +397,7 @@ let scanBack (folder: 'T -> 'State -> 'State) (xs: 'T list) (state: 'State) = let inline private uncheckedTail (xs: 'T list) : 'T list = nativeOnly let append (xs: 'T list) (ys: 'T list) = - let root = List.Empty + let root = newRoot () let mutable node = root let mutable rest = xs @@ -411,7 +409,7 @@ let append (xs: 'T list) (ys: 'T list) = root.Tail let collect (mapping: 'T -> 'U list) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let mutable node = root let mutable ys = xs @@ -424,30 +422,30 @@ let collect (mapping: 'T -> 'U list) (xs: 'T list) = ys <- ys.Tail - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapIndexed (mapping: int -> 'T -> 'U) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder i (acc: 'U list) x = acc.AppendConsNoTail(mapping i x) let node = foldIndexed folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let map (mapping: 'T -> 'U) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x = acc.AppendConsNoTail(mapping x) let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let indexed xs = mapIndexed (fun i x -> (i, x)) xs let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x y = acc.AppendConsNoTail(mapping x y) let node = fold2 folder root xs ys - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = @@ -458,9 +456,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) loop (i + 1) node xs.Tail ys.Tail - let root = List.Empty + let root = newRoot () let node = loop 0 root xs ys - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = @@ -471,9 +469,9 @@ let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: ' let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) loop node xs.Tail ys.Tail zs.Tail - let root = List.Empty + let root = newRoot () let node = loop root xs ys zs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let mapFold (mapping: 'State -> 'T -> 'Result * 'State) (state: 'State) (xs: 'T list) = @@ -481,9 +479,9 @@ let mapFold (mapping: 'State -> 'T -> 'Result * 'State) (state: 'State) (xs: 'T let r, st = mapping st x node.AppendConsNoTail r, st - let root = List.Empty + let root = newRoot () let node, state = fold folder (root, state) xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail, state let mapFoldBack (mapping: 'T -> 'State -> 'Result * 'State) (xs: 'T list) (state: 'State) = @@ -569,7 +567,7 @@ let tryItem n (xs: 'T list) = let item n (xs: 'T list) = xs.Item(n) let filter f (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'T list) x = if f x then @@ -578,11 +576,11 @@ let filter f (xs: 'T list) = acc let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let partition f (xs: 'T list) = - let root1, root2 = List.Empty, List.Empty + let root1, root2 = newRoot (), newRoot () let folder (lacc: 'T list, racc: 'T list) x = if f x then @@ -591,12 +589,12 @@ let partition f (xs: 'T list) = lacc, racc.AppendConsNoTail x let node1, node2 = fold folder (root1, root2) xs - node1.SetConsTail List.Empty - node2.SetConsTail List.Empty + node1.SetConsTail(empty ()) + node2.SetConsTail(empty ()) root1.Tail, root2.Tail let choose<'T, 'U> (f: 'T -> 'U option) (xs: 'T list) = - let root = List.Empty + let root = newRoot () let folder (acc: 'U list) x = match f x with @@ -604,20 +602,20 @@ let choose<'T, 'U> (f: 'T -> 'U option) (xs: 'T list) = | None -> acc let node = fold folder root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let contains (value: 'T) (xs: 'T list) ([] eq: System.Collections.Generic.IEqualityComparer<'T>) = tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = - let root = List.Empty + let root = newRoot () let mutable node = root for i = 0 to n - 1 do node <- node.AppendConsNoTail(f i) - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let replicate n x = initialize n (fun _ -> x) @@ -648,13 +646,13 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = | _ -> invalidArg "list2" SR.differentLengths let unzip xs = - foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) + foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (empty (), empty ()) let unzip3 xs = foldBack (fun (x, y, z) (lacc, macc, racc) -> List.Cons(x, lacc), List.Cons(y, macc), List.Cons(z, racc)) xs - (List.Empty, List.Empty, List.Empty) + (empty (), empty (), empty ()) let zip xs ys = map2 (fun x y -> x, y) xs ys @@ -754,12 +752,12 @@ let chunkBySize (chunkSize: int) (xs: 'T list) : 'T list list = toArray xs |> Array.chunkBySize chunkSize |> Array.map ofArray |> ofArray let allPairs (xs: 'T1 list) (ys: 'T2 list) : ('T1 * 'T2) list = - let root = List.Empty + let root = newRoot () let mutable node = root iterate (fun x -> iterate (fun y -> node <- node.AppendConsNoTail(x, y)) ys) xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let rec skip count (xs: 'T list) = @@ -790,9 +788,9 @@ let take count (xs: 'T list) = else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop count root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let takeWhile predicate (xs: 'T list) = @@ -804,9 +802,9 @@ let takeWhile predicate (xs: 'T list) = else loop (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let truncate count (xs: 'T list) = @@ -818,9 +816,9 @@ let truncate count (xs: 'T list) = else loop (i - 1) (acc.AppendConsNoTail xs.Head) xs.Tail - let root = List.Empty + let root = newRoot () let node = loop count root xs - node.SetConsTail List.Empty + node.SetConsTail(empty ()) root.Tail let getSlice (startIndex: int option) (endIndex: int option) (xs: 'T list) = @@ -843,7 +841,7 @@ let getSlice (startIndex: int option) (endIndex: int option) (xs: 'T list) = index if endIndex < startIndex then - List.Empty + (empty ()) else xs |> skip startIndex |> take (endIndex - startIndex + 1) @@ -904,7 +902,7 @@ let insertAt (index: int) (y: 'T) (xs: 'T list) : 'T list = let mutable isDone = false let result = - (List.Empty, xs) + (empty (), xs) ||> fold (fun acc x -> i <- i + 1 @@ -931,7 +929,7 @@ let insertManyAt (index: int) (ys: seq<'T>) (xs: 'T list) : 'T list = let ys = ofSeq ys let result = - (List.Empty, xs) + (empty (), xs) ||> fold (fun acc x -> i <- i + 1 From 71a0ad92237586e9569fd1483f35c88f8477f478 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 15:58:03 +0100 Subject: [PATCH 251/322] perf(js/ts): compile isNull to a plain null check (#5186) --- src/Fable.Transforms/Replacements.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index cf7eccd048..673aefef71 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1565,7 +1565,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o _ -> fsFormat com ctx r t i thisArg args | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args - | ("IsNull" | "IsNotNull" | "IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), + | ("IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions | "FailWith", [ msg ] -> makeThrow r t (error com msg) |> Some @@ -1679,6 +1679,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | (Operators.inequality | "Neq"), [ left; right ] -> equals com ctx r false left right |> Some | (Operators.equality | "Eq"), [ left; right ] -> equals com ctx r true left right |> Some | "IsNull", [ arg ] -> nullCheck r true arg |> Some + | "IsNotNull", [ arg ] -> nullCheck r false arg |> Some | "Hash", [ arg ] -> structuralHash com r arg |> Some // Comparison | "Compare", [ left; right ] -> compare com ctx r left right |> Some From 435fc0bb008ff2ab7c14a08c7218be94f4735029 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 17:14:17 +0100 Subject: [PATCH 252/322] perf(js/ts): check array bounds only on undefined reads (#5188) --- src/fable-library-ts/Array.fs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index e6832958fa..fd7cbe5a3a 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -932,10 +932,13 @@ let tail (array: 'T[]) = skipImpl array 1 let item (index: int) (array: 'T[]) : 'T = - if index < 0 || index >= array.Length then + let value = arrayAccessImpl index array + + // Only an out-of-range index reads undefined, so the bounds are checked on that path alone + if emitJsExpr value "$0 === undefined" && (index < 0 || index >= array.Length) then invalidArg "index" "Index was outside the bounds of the array." else - arrayAccessImpl index array + value // Using `set` seems to cause issues with the TS let setItem (array: 'T[]) (index: int) (value: 'T) = From 91bca925757356191c7e6e7a71cb3e8a1bc8d52d Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 17:14:44 +0100 Subject: [PATCH 253/322] perf(js/ts): closure-free ref cells and out-arg value cells (#5189) --- src/Fable.Transforms/FSharp2Fable.fs | 20 ++++++++++++++++++++ src/fable-library-ts/TimeOnly.ts | 2 +- src/fable-library-ts/Types.ts | 22 +++++++++++++++------- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 5e577af0c9..4153d5f4e2 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -767,6 +767,26 @@ let rec private transformDecisionTargets let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fsExpr = trampoline { match fsExpr with + // TryGetValue-style out args: pass a value cell and read it back, instead of a cell + // wrapping the local in a getter and a setter closure + | ByrefArgToTuple(callee, memb, ownerGenArgs, membGenArgs, membArgs) when + com.Options.Language = JavaScript || com.Options.Language = TypeScript + -> + let r = makeRangeFrom fsExpr + let! callee = transformExprOpt com ctx callee + let! args = transformExprList com ctx membArgs + let args, def = List.splitLast args + let cellValue = Replacements.Api.makeRefCellFromValue com r def + let cellIdent = getIdentUniqueName com ctx "cell" |> makeTypedIdent cellValue.Type + let cellExpr = Fable.IdentExpr cellIdent + let genArgs = ownerGenArgs @ membGenArgs |> List.map (makeType ctx.GenericArgs) + let returnType = makeType ctx.GenericArgs fsExpr.Type.GenericArguments[0] + + let callExpr = + makeCallFrom com ctx r returnType genArgs callee (args @ [ cellExpr ]) memb + + let readBack = Replacements.Api.getRefCell com r def.Type cellExpr + return Fable.Let(cellIdent, cellValue, Fable.Value(Fable.NewTuple([ callExpr; readBack ], false), r)) // | ByrefArgToTuple (callee, memb, ownerGenArgs, membGenArgs, membArgs) -> // let! callee = transformExprOpt com ctx callee // let! args = transformExprList com ctx membArgs diff --git a/src/fable-library-ts/TimeOnly.ts b/src/fable-library-ts/TimeOnly.ts index 7a80ba21cd..f788c0e158 100644 --- a/src/fable-library-ts/TimeOnly.ts +++ b/src/fable-library-ts/TimeOnly.ts @@ -39,7 +39,7 @@ export function add(t: number, ts: number, wrappedDays?: FSharpRef) { return t2 < 0 ? millisecondsPerDay + t2 : t2; } - wrappedDays.contents = ts / millisecondsPerDay; + wrappedDays.contents = Math.trunc(ts / millisecondsPerDay); let newMs = t + ts % millisecondsPerDay; if (newMs < 0) { diff --git a/src/fable-library-ts/Types.ts b/src/fable-library-ts/Types.ts index dee38b1030..7c7e914143 100644 --- a/src/fable-library-ts/Types.ts +++ b/src/fable-library-ts/Types.ts @@ -174,24 +174,32 @@ export abstract class Record implements IEquatable, IComparable, } export class FSharpRef { - private readonly getter: () => T; - private readonly setter: (v: T) => void; + private readonly getter: (() => T) | undefined; + private readonly setter: ((v: T) => void) | undefined; + private value: T | undefined; get contents() { - return this.getter(); + return this.getter === undefined ? this.value as T : this.getter(); } set contents(v) { - this.setter(v) + if (this.setter === undefined) { + this.value = v; + } else { + this.setter(v); + } } + // Same field layout on both paths so every cell shares one hidden class constructor(contentsOrGetter: T | (() => T), setter?: (v: T) => void) { if (typeof setter === "function") { this.getter = contentsOrGetter as () => T; - this.setter = setter + this.setter = setter; + this.value = undefined; } else { - this.getter = () => contentsOrGetter as T; - this.setter = (v) => { contentsOrGetter = v }; + this.getter = undefined; + this.setter = undefined; + this.value = contentsOrGetter as T; } } } From bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 17:42:54 +0100 Subject: [PATCH 254/322] perf(js/ts): keep the curried original on its adapter (#5191) --- src/fable-library-ts/Util.ts | 84 +++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 8276898e93..152ab5a8c8 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -733,28 +733,34 @@ export function clear(col: Iterable): void { } } -const curried = new WeakMap(); +// The original curried function rides on its adapter so curryN can hand it back +const curriedKey = Symbol("curried"); +type CurriedAdapter = { [curriedKey]?: object }; + +function getCurried(f: object | null | undefined): object | undefined { + return f == null ? undefined : (f as CurriedAdapter)[curriedKey]; +} export function uncurry2(f: (a1: T1) => (a2: T2) => TResult) { if (f == null) { return null as unknown as (a1: T1, a2: T2) => TResult } const f2 = (a1: T1, a2: T2) => f(a1)(a2); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry2(f: (a1: T1, a2: T2) => TResult) { - return curried.get(f) as ((a1: T1) => (a2: T2) => TResult) ?? ((a1: T1) => (a2: T2) => f(a1, a2)); + return getCurried(f) as ((a1: T1) => (a2: T2) => TResult) ?? ((a1: T1) => (a2: T2) => f(a1, a2)); } export function uncurry3(f: (a1: T1) => (a2: T2) => (a3: T3) => TResult) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3) => TResult } const f2 = (a1: T1, a2: T2, a3: T3) => f(a1)(a2)(a3); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry3(f: (a1: T1, a2: T2, a3: T3) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => f(a1, a2, a3)); } @@ -763,12 +769,12 @@ export function uncurry4( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4) => f(a1)(a2)(a3)(a4); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry4(f: (a1: T1, a2: T2, a3: T3, a4: T4) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => f(a1, a2, a3, a4)); } @@ -777,12 +783,12 @@ export function uncurry5( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => f(a1)(a2)(a3)(a4)(a5); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry5(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => f(a1, a2, a3, a4, a5)); } @@ -791,12 +797,12 @@ export function uncurry6( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => f(a1)(a2)(a3)(a4)(a5)(a6); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry6(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => f(a1, a2, a3, a4, a5, a6)); } @@ -805,12 +811,12 @@ export function uncurry7( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry7(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => f(a1, a2, a3, a4, a5, a6, a7)); } @@ -819,12 +825,12 @@ export function uncurry8( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry8(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => f(a1, a2, a3, a4, a5, a6, a7, a8)); } @@ -833,12 +839,12 @@ export function uncurry9( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry9(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9)); } @@ -847,12 +853,12 @@ export function uncurry10( ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry10(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)); } @@ -861,12 +867,12 @@ export function uncurry11 ) { if (f == null) { return null as unknown as (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry11(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)); } @@ -875,12 +881,12 @@ export function uncurry12 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry12(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)); } @@ -889,12 +895,12 @@ export function uncurry13 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry13(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)); } @@ -903,12 +909,12 @@ export function uncurry14 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry14(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)); } @@ -917,12 +923,12 @@ export function uncurry15 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry15(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)); } @@ -931,12 +937,12 @@ export function uncurry16 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry16(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)); } @@ -945,12 +951,12 @@ export function uncurry17 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry17(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17)); } @@ -959,12 +965,12 @@ export function uncurry18 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry18(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18)); } @@ -973,12 +979,12 @@ export function uncurry19 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18)(a19); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry19(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)); } @@ -987,12 +993,12 @@ export function uncurry20 TResult } const f2 = (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20) => f(a1)(a2)(a3)(a4)(a5)(a6)(a7)(a8)(a9)(a10)(a11)(a12)(a13)(a14)(a15)(a16)(a17)(a18)(a19)(a20); - curried.set(f2, f); + (f2 as CurriedAdapter)[curriedKey] = f; return f2; } export function curry20(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20) => TResult) { - return curried.get(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => TResult + return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)); } From d88621c3b9d6ce48a72dc09b86a6805f612ac185 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 27 Oct 2026 18:07:37 +0100 Subject: [PATCH 255/322] perf(js/ts): walk arrays by index when enumerated as seq (#5192) --- src/fable-library-ts/Seq.fs | 11 +++++++++-- src/fable-library-ts/Util.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index fe33447e9a..cf14a251fd 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -311,9 +311,16 @@ let ofArray (arr: 'T[]) = arr :> seq<'T> let toArray (xs: seq<'T>) : 'T[] = match xs with - // | :? array<'T> as a -> Array.ofSeq a + | :? array<'T> as a -> Array.ofSeq a | :? list<'T> as a -> Array.ofList a - | _ -> Array.ofSeq xs + | _ -> + let res = ResizeArray<'T>() + use e = xs.GetEnumerator() + + while e.MoveNext() do + res.Add(e.Current) + + res.ToArray() let ofList (xs: list<'T>) = (xs :> seq<'T>) diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 152ab5a8c8..802e86dbbb 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -215,8 +215,39 @@ export function toEnumerable(e: IEnumerable | Iterable): IEnumerable else { return new Enumerable(new Enumerator(e[Symbol.iterator]())); } } +// Walks an array by index: the iterator protocol allocates a result object per element +export class ArrayEnumerator implements IEnumerator { + private readonly array: ArrayLike; + private current: T = defaultOf(); + private index = -1; + constructor(array: ArrayLike) { this.array = array; } + public ["System.Collections.Generic.IEnumerator`1.get_Current"]() { + return this.current; + } + public ["System.Collections.IEnumerator.get_Current"]() { + return this.current; + } + public ["System.Collections.IEnumerator.MoveNext"]() { + const i = ++this.index; + if (i < this.array.length) { + this.current = this.array[i]; + return true; + } + this.current = defaultOf(); + return false; + } + public ["System.Collections.IEnumerator.Reset"]() { + this.index = -1; + this.current = defaultOf(); + } + public Dispose() { + return; + } +} + export function getEnumerator(e: IEnumerable | Iterable): IEnumerator { if (isEnumerable(e)) { return e.GetEnumerator(); } + else if (Array.isArray(e)) { return new ArrayEnumerator(e); } else { return new Enumerator(e[Symbol.iterator]()); } } From e6a742b1ebda71342d4445705ea9198c2059ae90 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 28 Oct 2026 16:22:08 +0100 Subject: [PATCH 256/322] perf(js/ts): throw invalidArg and invalidOp at the call site (#5194) --- src/Fable.Transforms/Replacements.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 673aefef71..1a0fd41992 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1563,8 +1563,10 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Strings | ("PrintFormatToString" | "PrintFormatToStringThen" | "PrintFormat" | "PrintFormatLine" | "PrintFormatToError" | "PrintFormatLineToError" | "PrintFormatThen" | "PrintFormatToStringThenFail" | "PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _ -> fsFormat com ctx r t i thisArg args - | ("Failure" | "FailurePattern" | "InvalidArg" | "InvalidOp" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> + | ("Failure" | "FailurePattern" | "LazyPattern" | "Lock" | "NullArg" | "Using"), _ -> fsharpModule com ctx r t i thisArg args + // Thrown at the call site so V8 sees a non-returning branch rather than a call + | ("InvalidArg" | "InvalidOp"), _ -> fsharpModule com ctx r t i thisArg args |> Option.map (makeThrow r t) | ("IsNullV" | "NonNull" | "NonNullV" | "NullMatchPattern" | "NullValueMatchPattern" | "NonNullQuickPattern" | "NonNullQuickValuePattern" | "WithNull" | "WithNullV" | "NullV" | "NullArgCheck"), _ -> fsharpModule com ctx r t i thisArg args // Exceptions From 2b66a5d2184f3a35f0d73fb397ab902e300ac4bd Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 28 Oct 2026 16:22:24 +0100 Subject: [PATCH 257/322] perf(js/ts): hash unions without allocating (#5195) --- src/fable-library-ts/Types.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fable-library-ts/Types.ts b/src/fable-library-ts/Types.ts index 7c7e914143..faf4c05718 100644 --- a/src/fable-library-ts/Types.ts +++ b/src/fable-library-ts/Types.ts @@ -79,9 +79,11 @@ export abstract class Union implements } public GetHashCode() { - const hashes = this.fields.map((x: any) => structuralHash(x)); - hashes.splice(0, 0, numberHash(this.tag)); - return combineHashCodes(hashes); + let h = combineHashCode(0, numberHash(this.tag)); + for (let i = 0; i < this.fields.length; i++) { + h = combineHashCode(h, structuralHash(this.fields[i])); + } + return h; } public Equals(other: Union) { From d96b6ab82f0181be4a379c806abcedde23f1359a Mon Sep 17 00:00:00 2001 From: Goswin Date: Wed, 28 Oct 2026 17:55:02 +0100 Subject: [PATCH 258/322] fix(js/ts): zero-fill Array.zeroCreate for numeric types without a typed array (#5211) Co-authored-by: Claude Opus 5.5 --- src/Fable.Build/Test/TypeScript.fs | 3 +- src/Fable.Transforms/Replacements.fs | 2 +- tests/Js/Main/ArrayTests.fs | 65 ++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/Fable.Build/Test/TypeScript.fs b/src/Fable.Build/Test/TypeScript.fs index 8755d34876..097ea704b5 100644 --- a/src/Fable.Build/Test/TypeScript.fs +++ b/src/Fable.Build/Test/TypeScript.fs @@ -36,7 +36,8 @@ let handle (args: string list) = |> CmdLine.appendPrefix "--outDir" fableDest |> CmdLine.appendPrefix "--lang" "typescript" |> CmdLine.appendPrefix "--exclude" "Fable.Core" - // |> CmdLine.appendPrefix "--typedArrays" "false" + // JavaScript tests use the default typed arrays, so cover the other mode here + |> CmdLine.appendPrefix "--typedArrays" "false" |> CmdLine.appendRaw "--noCache" // Let Fable handle the TypeScript invocation diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 1a0fd41992..e28219ba98 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2375,7 +2375,7 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex let createArray size value = match t, value with - | Array(Number _ as t2, _), None when com.Options.TypedArrays -> newArrayAlloc size t2 + | Array(TypedArrayCompatible com MutableArray _ as t2, _), None -> newArrayAlloc size t2 | Array(t2, _), value -> let value = value |> Option.defaultWith (fun () -> getZero com ctx t2) // If we don't fill the array some operations may behave unexpectedly, like Array.prototype.reduce diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 9f5fc8cbac..33b77aebc7 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -14,6 +14,10 @@ open Fable.Core [] let jsConstructorIs (s: string) (ar: 'T[]) = true + +/// Expected JS constructor name of a numeric array, depending on the --typedArrays option +let typedArrayOr (typedArrayName: string) = + if Compiler.typedArrays then typedArrayName else "Array" #endif type ExceptFoo = { Bar:string } @@ -102,6 +106,67 @@ let tests = xs |> Array.sum |> equal 6 #endif +#if FABLE_COMPILER + testCase "Numeric array literals respect --typedArrays" <| fun () -> + [| 1y |] |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + [| 1uy |] |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + [| 1s |] |> jsConstructorIs (typedArrayOr "Int16Array") |> equal true + [| 1us |] |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + [| 1 |] |> jsConstructorIs (typedArrayOr "Int32Array") |> equal true + [| 1u |] |> jsConstructorIs (typedArrayOr "Uint32Array") |> equal true + [| 1L |] |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + [| 1UL |] |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + [| 1.f |] |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + [| 1. |] |> jsConstructorIs (typedArrayOr "Float64Array") |> equal true + [| 1M |] |> jsConstructorIs "Array" |> equal true + [| 1I |] |> jsConstructorIs "Array" |> equal true + [| 'a' |] |> jsConstructorIs "Array" |> equal true + + testCase "Array.zeroCreate respects --typedArrays" <| fun () -> + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int16Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Int32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Uint32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs (typedArrayOr "Float64Array") |> equal true + Array.zeroCreate 2 |> jsConstructorIs "Array" |> equal true + Array.zeroCreate 2 |> equal [| 0L; 0L |] + Array.zeroCreate 2 |> equal [| 0.; 0. |] + Array.zeroCreate 2 |> equal [| 0M; 0M |] + + testCase "Array module functions respect --typedArrays" <| fun () -> + let xs = Array.init 3 int8 + xs |> jsConstructorIs (typedArrayOr "Int8Array") |> equal true + xs |> equal [| 0y; 1y; 2y |] + let xs = Array.create 2 7us + xs |> jsConstructorIs (typedArrayOr "Uint16Array") |> equal true + xs |> equal [| 7us; 7us |] + let xs = Array.ofSeq (seq { 1L; 2L }) + xs |> jsConstructorIs (typedArrayOr "BigInt64Array") |> equal true + xs |> Array.sum |> equal 3L + let xs = Array.ofSeq (seq { 1UL; 2UL }) + xs |> jsConstructorIs (typedArrayOr "BigUint64Array") |> equal true + xs |> Array.sum |> equal 3UL + let xs = [| 1; 2 |] |> Array.map float32 + xs |> jsConstructorIs (typedArrayOr "Float32Array") |> equal true + xs |> equal [| 1.f; 2.f |] + let xs = [| 1.5; 2.5 |] |> Array.map string + xs |> jsConstructorIs "Array" |> equal true + xs |> equal [| "1.5"; "2.5" |] + + testCase "Byte arrays respect --typedArrays" <| fun () -> + let xs = System.Text.Encoding.UTF8.GetBytes("ab") + xs |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + xs |> equal [| 97uy; 98uy |] + let xs = System.BitConverter.GetBytes(1s) + xs |> jsConstructorIs (typedArrayOr "Uint8Array") |> equal true + xs |> equal [| 1uy; 0uy |] +#endif + testCase "Mapping from values to functions works" <| fun () -> let a = [| "a"; "b"; "c" |] let b = [| 1; 2; 3 |] From 9517eafd93aa7b84061662a421b7a39a7269318b Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 28 Oct 2026 17:55:25 +0100 Subject: [PATCH 259/322] perf(js/ts): probe dictionary buckets without allocating (#5196) --- src/fable-library-ts/MutableMap.fs | 132 +++++++++++++++++++---------- src/fable-library-ts/MutableSet.fs | 99 ++++++++++++++-------- src/fable-library-ts/Util.ts | 9 +- tests/Js/Main/DictionaryTests.fs | 32 +++++++ tests/Js/Main/HashSetTests.fs | 18 ++++ 5 files changed, 204 insertions(+), 86 deletions(-) diff --git a/src/fable-library-ts/MutableMap.fs b/src/fable-library-ts/MutableMap.fs index 04b1633556..9ed742434a 100644 --- a/src/fable-library-ts/MutableMap.fs +++ b/src/fable-library-ts/MutableMap.fs @@ -12,6 +12,7 @@ type MutableMap<'Key, 'Value when 'Key: equality> // Compiles to JS Map of key hashes pointing to dynamic arrays of KeyValuePair<'Key, 'Value>. let hashMap = Dictionary>>() + let mutable count = 0 do for pair in pairs do @@ -20,29 +21,43 @@ type MutableMap<'Key, 'Value when 'Key: equality> // new () = MutableMap (Seq.empty, EqualityComparer.Default) // new (comparer) = MutableMap (Seq.empty, comparer) - member private this.TryFindIndex(k) = - let h = comparer.GetHashCode(k) + // An erased option of a reference type, so a hit allocates nothing + member private this.TryFindBucket(h: int) : ResizeArray> option = + if hashMap.ContainsKey h then + Some hashMap.[h] + else + None - match hashMap.TryGetValue h with - | true, pairs -> true, h, pairs.FindIndex(fun pair -> comparer.Equals(k, pair.Key)) - | false, _ -> false, h, -1 + member private this.FindIndex(k, bucket: ResizeArray>) = + let mutable i = 0 + let mutable found = -1 - member this.TryFind(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> Some hashMap.[h].[i] - | _, _, _ -> None + while found < 0 && i < bucket.Count do + if comparer.Equals(k, bucket.[i].Key) then + found <- i - member this.Comparer = comparer + i <- i + 1 + + found - member this.Clear() = hashMap.Clear() + member this.TryFind(k: 'Key) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> + let i = this.FindIndex(k, bucket) - member this.Count = - let mutable count = 0 + if i > -1 then + Some bucket.[i] + else + None + | None -> None + + member this.Comparer = comparer - for pairs in hashMap.Values do - count <- count + pairs.Count + member this.Clear() = + hashMap.Clear() + count <- 0 - count + member this.Count = count member this.Item with get (k: 'Key) = @@ -50,36 +65,61 @@ type MutableMap<'Key, 'Value when 'Key: equality> | Some pair -> pair.Value | _ -> raise (KeyNotFoundException("The item was not found in collection")) and set (k: 'Key) (v: 'Value) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> hashMap.[h].[i] <- KeyValuePair(k, v) // replace - | true, h, _ -> hashMap.[h].Add(KeyValuePair(k, v)) |> ignore // append - | false, h, _ -> hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) - - member this.Add(k, v) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - let msg = - System.String.Format("An item with the same key has already been added. Key: {0}", k) - - raise (System.ArgumentException(msg)) - | true, h, _ -> hashMap.[h].Add(KeyValuePair(k, v)) |> ignore // append - | false, h, _ -> hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) // add new - - member this.ContainsKey(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> true - | _, _, _ -> false - - member this.Remove(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - hashMap.[h].RemoveAt(i) - - if hashMap.[h].Count = 0 then - hashMap.Remove(h) |> ignore - - true - | _, _, _ -> false + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) + + if i > -1 then + bucket.[i] <- KeyValuePair(k, v) + else + bucket.Add(KeyValuePair(k, v)) + count <- count + 1 + | None -> + hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) + count <- count + 1 + + member this.Add(k: 'Key, v: 'Value) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + if this.FindIndex(k, bucket) > -1 then + let msg = + System.String.Format("An item with the same key has already been added. Key: {0}", k) + + raise (System.ArgumentException(msg)) + + bucket.Add(KeyValuePair(k, v)) + count <- count + 1 + | None -> + hashMap.[h] <- ResizeArray([| KeyValuePair(k, v) |]) + count <- count + 1 + + member this.ContainsKey(k: 'Key) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> this.FindIndex(k, bucket) > -1 + | None -> false + + member this.Remove(k: 'Key) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) + + if i > -1 then + bucket.RemoveAt(i) + count <- count - 1 + + if bucket.Count = 0 then + hashMap.Remove(h) |> ignore + + true + else + false + | None -> false interface Fable.Core.Symbol_wellknown with member _.``Symbol.toStringTag`` = "Dictionary" diff --git a/src/fable-library-ts/MutableSet.fs b/src/fable-library-ts/MutableSet.fs index 100eeb520a..69f2bb780f 100644 --- a/src/fable-library-ts/MutableSet.fs +++ b/src/fable-library-ts/MutableSet.fs @@ -10,6 +10,7 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer // Compiles to JS Map of key hashes pointing to dynamic arrays of 'T. let hashMap = Dictionary>() + let mutable count = 0 do for item in items do @@ -18,55 +19,83 @@ type MutableSet<'T when 'T: equality>(items: 'T seq, comparer: IEqualityComparer // new () = MutableSet (Seq.empty, EqualityComparer.Default) // new (comparer) = MutableSet (Seq.empty, comparer) - member private this.TryFindIndex(k) = - let h = comparer.GetHashCode(k) + // An erased option of a reference type, so a hit allocates nothing + member private this.TryFindBucket(h: int) : ResizeArray<'T> option = + if hashMap.ContainsKey h then + Some hashMap.[h] + else + None - match hashMap.TryGetValue h with - | true, values -> true, h, values.FindIndex(fun v -> comparer.Equals(k, v)) - | false, _ -> false, h, -1 + member private this.FindIndex(k, bucket: ResizeArray<'T>) = + let mutable i = 0 + let mutable found = -1 - member private this.TryFind(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> Some hashMap.[h].[i] - | _, _, _ -> None + while found < 0 && i < bucket.Count do + if comparer.Equals(k, bucket.[i]) then + found <- i - member this.Comparer = comparer + i <- i + 1 + + found - member this.Clear() = hashMap.Clear() + member private this.TryFind(k: 'T) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> + let i = this.FindIndex(k, bucket) - member this.Count = - let mutable count = 0 + if i > -1 then + Some bucket.[i] + else + None + | None -> None - for items in hashMap.Values do - count <- count + items.Count + member this.Comparer = comparer - count + member this.Clear() = + hashMap.Clear() + count <- 0 - member this.Add(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> false - | true, h, _ -> - hashMap.[h].Add(k) |> ignore - true - | false, h, _ -> + member this.Count = count + + member this.Add(k: 'T) = + let h = comparer.GetHashCode(k) + + match this.TryFindBucket h with + | Some bucket -> + if this.FindIndex(k, bucket) > -1 then + false + else + bucket.Add(k) + count <- count + 1 + true + | None -> hashMap.[h] <- ResizeArray([| k |]) + count <- count + 1 true - member this.Contains(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> true - | _, _, _ -> false + member this.Contains(k: 'T) = + match this.TryFindBucket(comparer.GetHashCode(k)) with + | Some bucket -> this.FindIndex(k, bucket) > -1 + | None -> false + + member this.Remove(k: 'T) = + let h = comparer.GetHashCode(k) - member this.Remove(k) = - match this.TryFindIndex(k) with - | true, h, i when i > -1 -> - hashMap.[h].RemoveAt(i) + match this.TryFindBucket h with + | Some bucket -> + let i = this.FindIndex(k, bucket) - if hashMap.[h].Count = 0 then - hashMap.Remove(h) |> ignore + if i > -1 then + bucket.RemoveAt(i) + count <- count - 1 - true - | _, _, _ -> false + if bucket.Count = 0 then + hashMap.Remove(h) |> ignore + + true + else + false + | None -> false interface Fable.Core.Symbol_wellknown with member _.``Symbol.toStringTag`` = "HashSet" diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 802e86dbbb..d8c3322424 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -475,12 +475,11 @@ export function dateHash(x: Date): number { } export function arrayHash(x: ArrayLike): number { - const len = x.length; - const hashes: number[] = new Array(len); - for (let i = 0; i < len; i++) { - hashes[i] = structuralHash(x[i]); + let h = 0; + for (let i = 0; i < x.length; i++) { + h = combineHashCode(h, structuralHash(x[i])); } - return combineHashCodes(hashes); + return h; } export function structuralHash(x: T): number { diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index ea58422792..a3a86a9340 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -196,6 +196,38 @@ let tests = dic.Remove("C") |> equal false dic.Count |> equal 1 + testCase "Dictionary works when keys share a hash" <| fun () -> + let comparer = + { new IEqualityComparer with + member _.Equals(x, y) = x = y + member _.GetHashCode(_) = 1 } + let dic = Dictionary<_,_>(comparer) + dic.Add("A", 1) + dic.Add("B", 2) + dic.Add("C", 3) + dic.Count |> equal 3 + dic.["C"] |> equal 3 + dic.ContainsKey("B") |> equal true + dic.ContainsKey("D") |> equal false + dic.Remove("B") |> equal true + dic.Remove("B") |> equal false + dic.ContainsKey("C") |> equal true + dic.Count |> equal 2 + throwsAnyError (fun _ -> dic.Add("A", 4)) + + testCase "Dictionary.Count works with indexer insert" <| fun () -> + let dic = Dictionary() + dic.["A"] <- 1 + dic.["B"] <- 2 + dic.Count |> equal 2 + + testCase "Dictionary.Count works with indexer replace" <| fun () -> + let dic = Dictionary() + dic.Add("A", 1) + dic.["A"] <- 2 + dic.Count |> equal 1 + dic.["A"] |> equal 2 + testCase "Dictionary.Remove with complex keys works" <| fun () -> let dic = Dictionary<_, _>() dic.Add((0,"A"), "Hello") diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index 3db49dccc3..ff8953b1bb 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -89,6 +89,24 @@ let tests = xs.Remove 3 |> equal false xs.Count |> equal 2 + testCase "HashSet works when items share a hash" <| fun () -> + let comparer = + { new IEqualityComparer with + member _.Equals(x, y) = x = y + member _.GetHashCode(_) = 1 } + let xs = HashSet<_>(comparer) + xs.Add "A" |> equal true + xs.Add "B" |> equal true + xs.Add "C" |> equal true + xs.Add "B" |> equal false + xs.Count |> equal 3 + xs.Contains "C" |> equal true + xs.Contains "D" |> equal false + xs.Remove "B" |> equal true + xs.Remove "B" |> equal false + xs.Contains "C" |> equal true + xs.Count |> equal 2 + testCase "HashSet.UnionWith works" <| fun () -> let xs = hashSet [1; 2] let ys = hashSet [2; 4] From d5f1f8fdfab535eaff5c492661d0178be1c4c76a Mon Sep 17 00:00:00 2001 From: Goswin Date: Wed, 28 Oct 2026 18:15:38 +0100 Subject: [PATCH 260/322] fix(js/ts): throw on out-of-range string indexer (#5214) Co-authored-by: Claude Opus 5.5 --- src/Fable.Transforms/Replacements.fs | 4 +++- tests/Js/Main/StringTests.fs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index e28219ba98..80c7a1de2d 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3040,7 +3040,9 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr |> withTag "downcast" |> Some | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args - | "GetString", _, [ ar; idx ] -> getExpr r t ar idx |> Some + | "GetString", _, [ str; idx ] -> + Helper.LibCall(com, "String", "getCharAtIndex", t, [ str; idx ], i.SignatureArgTypes, ?loc = r) + |> Some | "GetArray", _, [ ar; idx ] -> arrayGetItem com r t [ idx; ar ] (i.SignatureArgTypes |> List.rev) | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 8a5b9e4929..b32397991e 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -1163,6 +1163,25 @@ let tests = testList "Strings" [ input.Chars(2) |> equal 'l' + testCase "String.Chars throws when index is out of bounds" <| fun () -> + let input = "abc" + throwsAnyError <| fun () -> + input.Chars(-1) |> ignore + throwsAnyError <| fun () -> + input.Chars(3) |> ignore + + testCase "String indexer works" <| fun () -> + let input = "hello" + input.[1] |> equal 'e' + input.[4] |> equal 'o' + + testCase "String indexer throws when index is out of bounds" <| fun () -> + let input = "abc" + throwsAnyError <| fun () -> + input.[-1] |> ignore + throwsAnyError <| fun () -> + input.[3] |> ignore + testCase "String.Substring works" <| fun () -> "abcdefg".Substring(2) |> equal "cdefg" From 5185e6547416384fe85fcb0b0c1cf267708879db Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Wed, 28 Oct 2026 13:07:51 -0700 Subject: [PATCH 261/322] fix(js/ts/python/dart/beam): add FSharp.Core's argument checks to collection functions (#5154) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Beam/Replacements.fs | 8 +- src/Fable.Transforms/Dart/Replacements.fs | 4 + src/Fable.Transforms/Replacements.fs | 16 ++- src/fable-library-beam/Seq.fs | 73 +++++++---- src/fable-library-beam/fable_list.erl | 114 ++++++++++++++++-- src/fable-library-dart/Array.fs | 97 +++++++++------ src/fable-library-dart/Global.fs | 1 + src/fable-library-dart/List.fs | 86 ++++++++----- src/fable-library-dart/Seq.fs | 79 ++++++++---- src/fable-library-py/fable_library/List.fs | 86 ++++++++----- src/fable-library-py/fable_library/Seq.fs | 82 ++++++++----- .../fable_library/seq_native.py | 3 + src/fable-library-ts/Array.fs | 114 +++++++++++------- src/fable-library-ts/List.fs | 86 ++++++++----- src/fable-library-ts/Seq.fs | 80 ++++++++---- tests/Beam/ArrayTests.fs | 49 ++++++++ tests/Beam/ListTests.fs | 30 +++++ tests/Beam/SeqTests.fs | 36 ++++++ tests/Dart/src/ArrayTests.fs | 42 +++++++ tests/Dart/src/ListTests.fs | 48 ++++++++ tests/Dart/src/SeqTests.fs | 38 ++++++ tests/Dart/src/Tests.Util.fs | 1 + tests/Js/Main/ArrayTests.fs | 85 +++++++++++++ tests/Js/Main/ListTests.fs | 59 +++++++++ tests/Js/Main/SeqTests.fs | 37 ++++++ tests/Js/Main/Util/Util.fs | 1 + tests/Python/TestList.fs | 70 +++++++++++ tests/Python/TestSeq.fs | 52 ++++++++ 28 files changed, 1174 insertions(+), 303 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index c0f298fca7..c0c94f7b81 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -2907,7 +2907,7 @@ let private arrayModule | ("Sub" | "GetSubArray"), [ arr; start; count ] -> let arr = derefArr r arr - emitExpr r t [ arr; start; count ] "lists:sublist($0, $1 + 1, $2)" + Helper.LibCall(com, "fable_list", "sub", t, [ arr; start; count ]) |> wrapArr com r t |> Some | "Except", [ excludeArr; arr ] -> @@ -2978,11 +2978,11 @@ let private arrayModule | "Exists2", [ fn; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 - Helper.LibCall(com, "fable_list", "exists2", t, [ fn; a1; a2 ]) |> Some + Helper.LibCall(com, "fable_list", "array_exists2", t, [ fn; a1; a2 ]) |> Some | "ForAll2", [ fn; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 - Helper.LibCall(com, "fable_list", "forall2", t, [ fn; a1; a2 ]) |> Some + Helper.LibCall(com, "fable_list", "array_forall2", t, [ fn; a1; a2 ]) |> Some | "Fold2", [ fn; state; a1; a2 ] -> let a1 = derefArr r a1 let a2 = derefArr r a2 @@ -3134,7 +3134,7 @@ let private arrayModule let blitted = Helper.LibCall( com, - "fable_resize_array", + "fable_list", "blit", t, [ derefSource; sourceIdx; derefTarget; targetIdx; count ] diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 66b24c4b58..354372048a 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -1877,6 +1877,10 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex | "IsEmpty", [ ar ] -> getFieldWith r t ar "isEmpty" |> Some | "Contains", [ value; ar ] -> Helper.LibCall(com, "Array", "contains", t, [ value; ar ], ?loc = r) |> Some | "CopyTo", args -> copyToArray com r t i args + // getSubArray is shared with ResizeArray.GetRange, which must stay unchecked + | "GetSubArray", args -> + Helper.LibCall(com, "Array", "sub", t, args, i.SignatureArgTypes, genArgs = i.GenericArgs, ?loc = r) + |> Some | ("Distinct" | "DistinctBy" | "Except" | "GroupBy" | "CountBy" as meth), args -> let meth = Naming.lowerFirst meth let args = injectArg com ctx r "Seq2" meth i.GenericArgs args diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 80c7a1de2d..616d36d473 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2339,14 +2339,26 @@ let tuples (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E | "ToTuple", _ -> changeKind false args | _ -> None +let arrayCopy (com: ICompiler) (ctx: Context) r t source sourceIndex target targetIndex count = + let toInt32 (arg: Expr) = + match arg.Type with + | Number(Int32, _) -> arg + | _ -> toInt com ctx r Int32.Number [ arg ] + + let args = + [ source; toInt32 sourceIndex; target; toInt32 targetIndex; toInt32 count ] + + Helper.LibCall(com, "Array", "copyTo", t, args, ?loc = r) |> Some + let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | "get_Length", Some arg, _ -> getFieldWith r t arg "length" |> Some | "get_Item", Some arg, [ idx ] -> getExpr r t arg idx |> Some | "set_Item", Some arg, [ idx; value ] -> setExpr r arg idx value |> Some - | "Copy", None, [ _source; _sourceIndex; _target; _targetIndex; _count ] -> copyToArray com r t i args + | "Copy", None, [ source; sourceIndex; target; targetIndex; count ] -> + arrayCopy com ctx r t source sourceIndex target targetIndex count | "Copy", None, [ source; target; count ] -> - copyToArray com r t i [ source; makeIntConst 0; target; makeIntConst 0; count ] + arrayCopy com ctx r t source (makeIntConst 0) target (makeIntConst 0) count | "ConvertAll", None, [ source; mapping ] -> Helper.LibCall(com, "Array", "map", t, [ mapping; source ], ?loc = r) |> Some | "IndexOf", None, args -> diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs index 71671d5394..d4e496146e 100644 --- a/src/fable-library-beam/Seq.fs +++ b/src/fable-library-beam/Seq.fs @@ -13,6 +13,7 @@ type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> module SR = let enumerationAlreadyFinished = "Enumeration already finished." let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." @@ -683,6 +684,9 @@ let head (xs: seq<'T>) = | None -> invalidArg "source" SR.inputSequenceEmpty let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -1002,7 +1006,12 @@ let reduceBack folder (xs: seq<'T>) = else invalidArg "source" SR.inputSequenceEmpty -let replicate n x = initialize n (fun _ -> x) +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) + + initialize n (fun _ -> x) let reverse (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -1074,6 +1083,9 @@ let tail (xs: seq<'T>) = ) let take count (xs: seq<'T>) = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + generateIndexed (fun () -> ofSeq xs) (fun i e -> @@ -1122,19 +1134,25 @@ let pairwise (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rec columns (rows: 'T list list) = + match rows |> List.filter (List.isEmpty >> not) with + | [] -> [] + | rows -> ofList (List.map List.head rows) :: columns (List.map List.tail rows) + + xss |> toList |> List.map toList |> columns |> ofList ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1238,6 +1256,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) // let init = initialize @@ -1342,28 +1363,28 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = if i < index then - if e.MoveNext() then - Some e.Current - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - if e.MoveNext() then - Some e.Current - else - None - ) - (fun e -> e.Dispose()) + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 580eb7c9e5..ea7d918ecb 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -47,6 +47,8 @@ pairwise/1, exists2/3, forall2/3, + array_exists2/3, + array_forall2/3, map2/3, map3/4, mapi2/3, @@ -79,6 +81,8 @@ remove_many_at/3, index_of_value/2, get_slice/3, + sub/3, + blit/5, take/2, random_shuffle_by/2, random_shuffle_with/2, @@ -134,6 +138,8 @@ -spec pairwise(list()) -> list({term(), term()}). -spec exists2(fun(), list(), list()) -> boolean(). -spec forall2(fun(), list(), list()) -> boolean(). +-spec array_exists2(fun(), list(), list()) -> boolean(). +-spec array_forall2(fun(), list(), list()) -> boolean(). -spec map2(fun(), list(), list()) -> list(). -spec map3(fun(), list(), list(), list()) -> list(). -spec mapi2(fun(), list(), list()) -> list(). @@ -147,9 +153,9 @@ -spec group_by(fun(), list()) -> list({term(), list()}). -spec unfold(fun(), term()) -> list(). -spec split_at(non_neg_integer(), list()) -> {list(), list()}. --spec chunk_by_size(pos_integer(), list()) -> list(list()). --spec windowed(pos_integer(), list()) -> list(list()). --spec split_into(pos_integer(), list()) -> list(list()). +-spec chunk_by_size(integer(), list()) -> list(list()). +-spec windowed(integer(), list()) -> list(list()). +-spec split_into(integer(), list()) -> list(list()). -spec except(list(), list()) -> list(). -spec all_pairs(list(), list()) -> list({term(), term()}). -spec permute(fun(), list()) -> list(). @@ -424,11 +430,37 @@ pairwise([]) -> []; pairwise([_]) -> []; pairwise([A, B | T]) -> [{A, B} | pairwise([B | T])]. -exists2(Fn, L1, L2) -> - lists:any(fun({A, B}) -> (Fn(A))(B) end, lists:zip(L1, L2)). - -forall2(Fn, L1, L2) -> - lists:all(fun({A, B}) -> (Fn(A))(B) end, lists:zip(L1, L2)). +exists2(Fn, [A | T1], [B | T2]) -> + case (Fn(A))(B) of + true -> true; + false -> exists2(Fn, T1, T2) + end; +exists2(_Fn, [], []) -> + false; +exists2(_Fn, _L1, _L2) -> + lists_had_different_lengths(<<"list2">>). + +forall2(Fn, [A | T1], [B | T2]) -> + case (Fn(A))(B) of + true -> forall2(Fn, T1, T2); + false -> false + end; +forall2(_Fn, [], []) -> + true; +forall2(_Fn, [], _L2) -> + lists_had_different_lengths(<<"list1">>); +forall2(_Fn, _L1, []) -> + lists_had_different_lengths(<<"list2">>). + +array_exists2(Fn, A1, A2) when length(A1) =:= length(A2) -> + exists2(Fn, A1, A2); +array_exists2(_Fn, _A1, _A2) -> + invalid_arg(<<"array1">>, <<"The arrays have different lengths.">>). + +array_forall2(Fn, A1, A2) when length(A1) =:= length(A2) -> + forall2(Fn, A1, A2); +array_forall2(_Fn, _A1, _A2) -> + invalid_arg(<<"array1">>, <<"The arrays have different lengths.">>). map2(Fn, L1, L2) -> lists:zipwith(fun(A, B) -> (Fn(A))(B) end, L1, L2). @@ -507,6 +539,8 @@ unfold(Fn, State) -> split_at(Index, List) -> {lists:sublist(List, Index), lists:nthtail(Index, List)}. +chunk_by_size(Size, _List) when Size < 1 -> + invalid_arg(<<"chunkSize">>, <<"The input must be positive.">>); chunk_by_size(Size, List) -> chunk_by_size(Size, List, []). chunk_by_size(_Size, [], Acc) -> @@ -519,6 +553,8 @@ chunk_by_size(Size, List, Acc) -> end, chunk_by_size(Size, Rest, [Chunk | Acc]). +windowed(Size, _List) when Size < 1 -> + invalid_arg(<<"windowSize">>, <<"The input must be positive.">>); windowed(Size, List) -> windowed(Size, List, []). windowed(Size, List, Acc) when length(List) < Size -> lists:reverse(Acc); @@ -526,6 +562,10 @@ windowed(Size, [_ | T] = List, Acc) -> Window = lists:sublist(List, Size), windowed(Size, T, [Window | Acc]). +split_into(Count, _List) when Count < 1 -> + invalid_arg(<<"count">>, <<"The input must be positive.">>); +split_into(_Count, []) -> + []; split_into(Count, List) -> Len = length(List), ActualCount = min(Count, Len), @@ -617,12 +657,19 @@ try_find_index_back(Fn, [H | T], Idx) -> transpose([]) -> []; -transpose([[] | _]) -> +transpose([First | Rest] = Lists) -> + Len = length(First), + case lists:all(fun(L) -> length(L) =:= Len end, Rest) of + true -> transpose_rows(Lists); + false -> invalid_arg(<<"array.[0]">>, <<"The arrays have different lengths.">>) + end. + +transpose_rows([[] | _]) -> []; -transpose(Lists) -> +transpose_rows(Lists) -> Heads = [hd(L) || L <- Lists], Tails = [tl(L) || L <- Lists], - [Heads | transpose(Tails)]. + [Heads | transpose_rows(Tails)]. compare_with(_Fn, [], []) -> 0; @@ -676,6 +723,42 @@ get_slice(Lower, Upper, List) -> end, lists:sublist(List, Start + 1, End - Start + 1). +-spec sub(list(), integer(), integer()) -> list(). +sub(_List, StartIndex, _Count) when StartIndex < 0 -> + invalid_arg(<<"startIndex">>, <<"The input must be non-negative.">>); +sub(_List, _StartIndex, Count) when Count < 0 -> + invalid_arg(<<"count">>, <<"The input must be non-negative.">>); +sub(List, 0, Count) -> + case lists:sublist(List, Count) of + Sub when length(Sub) =:= Count -> Sub; + _ -> argument_out_of_range(<<"count">>, <<"The index is outside the legal range.">>) + end; +sub([_ | T], StartIndex, Count) -> + sub(T, StartIndex - 1, Count); +sub([], _StartIndex, _Count) -> + argument_out_of_range(<<"count">>, <<"The index is outside the legal range.">>). + +%% HashSet.CopyTo calls fable_resize_array:blit/5 directly, its count may exceed the set +-spec blit(list(), integer(), list(), integer(), integer()) -> list(). +blit(_Source, _SourceIdx, _Target, _TargetIdx, Count) when Count < 0 -> + argument_out_of_range(<<"length">>, <<"Non-negative number required.">>); +blit(_Source, SourceIdx, _Target, _TargetIdx, _Count) when SourceIdx < 0 -> + argument_out_of_range(<<"sourceIndex">>, <<"Non-negative number required.">>); +blit(Source, SourceIdx, _Target, _TargetIdx, Count) when SourceIdx + Count > length(Source) -> + invalid_arg( + <<"sourceArray">>, + <<"Source array was not long enough. Check the source index, length, and the array's lower bounds.">> + ); +blit(_Source, _SourceIdx, _Target, TargetIdx, _Count) when TargetIdx < 0 -> + argument_out_of_range(<<"destinationIndex">>, <<"Non-negative number required.">>); +blit(_Source, _SourceIdx, Target, TargetIdx, Count) when TargetIdx + Count > length(Target) -> + invalid_arg( + <<"destinationArray">>, + <<"Destination array was not long enough. Check the destination index, length, and the array's lower bounds.">> + ); +blit(Source, SourceIdx, Target, TargetIdx, Count) -> + fable_resize_array:blit(Source, SourceIdx, Target, TargetIdx, Count). + -spec take(non_neg_integer(), list()) -> list(). take(Count, List) -> case erlang:length(List) >= Count of @@ -834,3 +917,12 @@ invalid_arg(ParamName, Message) -> exn_type => argument_exception, message => fable_utils:argument_exception_message(Message, ParamName) }). + +argument_out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). + +lists_had_different_lengths(ParamName) -> + invalid_arg(ParamName, <<"The lists had different lengths.">>). diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index db4e3746e8..1753bb1914 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -143,12 +143,36 @@ let filter (predicate: 'T -> bool) (array: 'T[]) = Native.where predicate array // intentionally returns target instead of unit let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = - Native.fillRange target targetIndex (targetIndex + count) value + if targetIndex < 0 then + invalidArg "targetIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + // .NET fills up to the end of the array before raising, this checks first + if count > 0 then + if targetIndex + count > target.Length then + raise (System.IndexOutOfRangeException(SR.Arg_IndexOutOfRangeException)) + + Native.fillRange target targetIndex (targetIndex + count) value + target let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = Native.sublist (array, start, start + count) +let sub (array: 'T[]) (startIndex: int) (count: int) : 'T[] = + if startIndex < 0 then + invalidArg "startIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + if startIndex + count > array.Length then + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) + + getSubArray array startIndex count + let last (array: 'T[]) = if Array.isEmpty array then invalidArg "array" LanguagePrimitives.ErrorStrings.InputArrayEmptyString @@ -400,8 +424,26 @@ let removeAllInPlace (predicate: 'T -> bool) (array: 'T[]) : int = Native.removeWhere array predicate len - array.Length -// TODO: Check array lengths let copyTo (source: 'T[]) (sourceIndex: int) (target: 'T[]) (targetIndex: int) (count: int) : unit = + if count < 0 then + raise (System.ArgumentOutOfRangeException("length", SR.Arg_ArgumentOutOfRangeException)) + + if sourceIndex < 0 then + raise (System.ArgumentOutOfRangeException("sourceIndex", SR.Arg_ArgumentOutOfRangeException)) + + if sourceIndex + count > source.Length then + invalidArg + "sourceArray" + "Source array was not long enough. Check the source index, length, and the array's lower bounds." + + if targetIndex < 0 then + raise (System.ArgumentOutOfRangeException("destinationIndex", SR.Arg_ArgumentOutOfRangeException)) + + if targetIndex + count > target.Length then + invalidArg + "destinationArray" + "Destination array was not long enough. Check the destination index, length, and the array's lower bounds." + Native.copyRange (target, targetIndex, source, sourceIndex, sourceIndex + count) let indexOf (array: 'T[]) (item: 'T) (start: int option) (count: int option) : int = @@ -739,7 +781,7 @@ let zip3 (array1: 'T1[]) (array2: 'T2[]) (array3: 'T3[]) : ('T1 * 'T2 * 'T3)[] = let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = if chunkSize < 1 then - invalidArg "size" "The input must be positive." + invalidArg "chunkSize" "The input must be positive." if Array.isEmpty array then [| [||] |] @@ -1030,7 +1072,7 @@ let windowed (windowSize: int) (source: 'T[]) : 'T[][] = let splitInto (chunks: int) (array: 'T[]) : 'T[][] = if chunks < 1 then - invalidArg "chunks" "The input must be positive." + invalidArg "count" "The input must be positive." if Array.isEmpty array then [| [||] |] @@ -1129,45 +1171,22 @@ let removeAt (index: int) (xs: 'T[]) : 'T[] = ) let removeManyAt (index: int) (count: int) (xs: 'T[]) : 'T[] = - let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 - - let ys = - xs - |> filter (fun _ -> - i <- i + 1 + let len = xs.Length - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true - ) + // Unlike FSharp.Core, a negative count removes nothing instead of duplicating items + if index < 0 || index > len - count || index > len then + invalidArg "index" "index must be within bounds of the array" - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status + let count = Operators.max 0 count - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" + Native.generate + (len - count) + (fun i -> + if i < index then + xs[i] else - "count" - - invalidArg arg SR.indexOutOfBounds - - ys + xs[i + count] + ) let updateAt (index: int) (y: 'T) (xs: 'T[]) : 'T[] = let len = xs.Length diff --git a/src/fable-library-dart/Global.fs b/src/fable-library-dart/Global.fs index 5da4b222b5..ce62f1f3f6 100644 --- a/src/fable-library-dart/Global.fs +++ b/src/fable-library-dart/Global.fs @@ -18,6 +18,7 @@ module SR = let inputWasEmpty = "The input list was empty." let inputMustBeNonNegative = "The input must be non-negative." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." diff --git a/src/fable-library-dart/List.fs b/src/fable-library-dart/List.fs index 50c0948e56..6d2d365faa 100644 --- a/src/fable-library-dart/List.fs +++ b/src/fable-library-dart/List.fs @@ -152,6 +152,9 @@ and List<'T> = LinkedList<'T> let inline indexNotFound () = raise (KeyNotFoundException(SR.keyNotFoundAlt)) +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." + let empty () = List.Empty let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) @@ -265,11 +268,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold<'T, 'State> (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -414,6 +435,11 @@ let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) @@ -427,6 +453,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) @@ -568,6 +597,9 @@ let contains (value: 'T) (xs: 'T list) ([] eq: IEqualityComparer<'T>) = tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + let root = List.Empty let mutable node = root @@ -593,8 +625,12 @@ let reduceBack f (xs: 'T list) = let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -602,7 +638,7 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) @@ -681,6 +717,9 @@ let min (xs: 'T list) ([] comparer: IComparer<'T>) : 'T = xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -691,6 +730,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -927,43 +969,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index dc22feb87f..6e63f866fe 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -630,6 +630,9 @@ let tryHead (xs: seq<'T>) : 'T option = None let initialize (count: int) (f: int -> 'a) : seq<'a> = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -954,7 +957,12 @@ let reduceBack (folder: 'T -> 'T -> 'T) (xs: seq<'T>) : 'T = Array.reduceBack folder arr -let replicate (n: int) (x: 'a) : seq<'a> = initialize n (fun _ -> x) +let replicate (n: int) (x: 'a) : seq<'a> = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (ArgumentOutOfRangeException("count", SR.Arg_ArgumentOutOfRangeException)) + + initialize n (fun _ -> x) let reverse (xs: seq<'T>) : seq<'T> = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -1026,6 +1034,9 @@ let tail (xs: seq<'T>) : seq<'T> = ) let take (count: int) (xs: seq<'T>) : seq<'T> = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + Enumerator.generateIndexed (fun () -> ofSeq xs) (fun i e -> @@ -1074,19 +1085,32 @@ let pairwise (xs: seq<'T>) : seq<'T * 'T> = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize <= 0 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray()) + + rows[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1186,6 +1210,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) let insertAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = @@ -1277,28 +1304,28 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - Enumerator.generateIndexed - (fun () -> ofSeq xs) - (fun i e -> + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = if i < index then - if e.MoveNext() then - Some(e.Current) - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - if e.MoveNext() then - Some(e.Current) - else - None - ) - (fun e -> e.Dispose()) + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false diff --git a/src/fable-library-py/fable_library/List.fs b/src/fable-library-py/fable_library/List.fs index 9859354847..c4d4f49048 100644 --- a/src/fable-library-py/fable_library/List.fs +++ b/src/fable-library-py/fable_library/List.fs @@ -167,6 +167,9 @@ and List<'T> = LinkedList<'T> let indexNotFound () = raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." + let empty () = List.Empty let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) @@ -281,11 +284,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -431,6 +452,11 @@ let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) @@ -444,6 +470,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) @@ -589,6 +618,9 @@ let contains (value: 'T) (xs: 'T list) ([] eq: System.Collections.Generi tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + let root = List.Empty let mutable node = root @@ -614,8 +646,12 @@ let reduceBack f (xs: 'T list) = let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -623,7 +659,7 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (List.Empty, List.Empty) @@ -706,6 +742,9 @@ let min (xs: 'T list) ([] comparer: System.Collections.Generic.IComparer xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -716,6 +755,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -952,43 +994,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index e15b92cd11..4456f22e7e 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -14,6 +14,7 @@ type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> module SR = let enumerationAlreadyFinished = "Enumeration already finished." let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." @@ -650,6 +651,9 @@ let forAll2 predicate xs ys = // `tryHead`/`head`: no F# impl - Replacements.fs redirects both straight to `seq_native`. let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -890,7 +894,12 @@ let reduceBack folder (xs: seq<'T>) = else invalidArg "source" SR.inputSequenceEmpty -let replicate n x = initialize n (fun _ -> x) +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) + + initialize n (fun _ -> x) let reverse (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -975,19 +984,32 @@ let pairwise (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray<'T>()) + + rows.[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1091,6 +1113,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) // let init = initialize @@ -1195,28 +1220,31 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> - if i < index then - if e.MoveNext() then - Some e.Current - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + let endIndex = index + count - if e.MoveNext() then - Some e.Current - else - None - ) - (fun e -> e.Dispose()) + mkSeq (fun () -> + let mutable i = -1 + + Enumerator.generateWhileSome + (fun () -> ofSeq xs) + (fun e -> + let mutable res = None + + while Option.isNone res && e.MoveNext() do + i <- i + 1 + + if i < index || i >= endIndex then + res <- Some e.Current + + if Option.isNone res && i < index then + invalidArg "index" "index must be within bounds of the array" + + res + ) + (fun e -> e.Dispose()) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false diff --git a/src/fable-library-py/fable_library/seq_native.py b/src/fable-library-py/fable_library/seq_native.py index f9a9750ce4..cbdcdf2f9a 100644 --- a/src/fable-library-py/fable_library/seq_native.py +++ b/src/fable-library-py/fable_library/seq_native.py @@ -161,6 +161,9 @@ def gen() -> Iterator[T]: def take[T](count: int, xs: IEnumerable_1[T]) -> NativeSeq[T]: + if count < 0: + raise ArgumentException__ctor_Z384F8060("The input must be non-negative.", "count") + def gen() -> Iterator[T]: it = _source_iter(xs) for _ in range(count): diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index fd7cbe5a3a..d1e0ee51e8 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -36,9 +36,30 @@ let append (array1: 'T[]) (array2: 'T[]) ([] cons: Con let filter (predicate: 'T -> bool) (array: 'T[]) = filterImpl predicate array // intentionally returns target instead of unit -let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = fillImpl target value targetIndex count +let fill (target: 'T[]) (targetIndex: int) (count: int) (value: 'T) : 'T[] = + if targetIndex < 0 then + invalidArg "targetIndex" SR.inputMustBeNonNegative -let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = subArrayImpl array start count + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + // .NET fills up to the end of the array before raising, this checks first + if count > 0 && targetIndex + count > target.Length then + raise (System.IndexOutOfRangeException()) + + fillImpl target value targetIndex count + +let getSubArray (array: 'T[]) (start: int) (count: int) : 'T[] = + if start < 0 then + invalidArg "startIndex" SR.inputMustBeNonNegative + + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + + if start + count > array.Length then + raise (System.ArgumentOutOfRangeException("count", "The index is outside the legal range.")) + + subArrayImpl array start count let last (array: 'T[]) = if array.Length = 0 then @@ -338,9 +359,44 @@ let copy (array: 'T[]) = // else copyImpl array +// Not inline: TypeScript accepts the typed array methods only on the obj (any) parameters +let private copyTypedArray (source: obj) (sourceIndex: int) (target: obj) (targetIndex: int) (count: int) : unit = + target?set (source?subarray (sourceIndex, sourceIndex + count), targetIndex) + +let private argumentOutOfRange (paramName: string) (value: int) (requirement: string) = + raise ( + System.ArgumentOutOfRangeException(paramName, paramName + " ('" + string value + "') must be " + requirement) + ) + +// System.Array.Copy compiles to this function, so it cannot call System.Array.Copy itself let copyTo (source: 'T[]) (sourceIndex: int) (target: 'T[]) (targetIndex: int) (count: int) = - // TODO: Check array lengths - System.Array.Copy(source, sourceIndex, target, targetIndex, count) + if count < 0 then + argumentOutOfRange "length" count "a non-negative value." + + if sourceIndex < 0 then + argumentOutOfRange "sourceIndex" sourceIndex "greater than or equal to '0'." + + if sourceIndex + count > source.Length then + invalidArg + "sourceArray" + "Source array was not long enough. Check the source index, length, and the array's lower bounds." + + if targetIndex < 0 then + argumentOutOfRange "destinationIndex" targetIndex "greater than or equal to '0'." + + if targetIndex + count > target.Length then + invalidArg + "destinationArray" + "Destination array was not long enough. Check the destination index, length, and the array's lower bounds." + + if isTypedArrayImpl source && isTypedArrayImpl target then + copyTypedArray (source :> obj) sourceIndex (target :> obj) targetIndex count + elif sourceIndex < targetIndex then + for i = count - 1 downto 0 do + arraySetItemImpl target (targetIndex + i) (arrayAccessImpl (sourceIndex + i) source) + else + for i = 0 to count - 1 do + arraySetItemImpl target (targetIndex + i) (arrayAccessImpl (sourceIndex + i) source) let reverse (array: 'T[]) = // if isTypedArrayImpl array then @@ -788,7 +844,7 @@ let zip3 (array1: 'T[]) (array2: 'U[]) (array3: 'V[]) = let chunkBySize (chunkSize: int) (array: 'T[]) : 'T[][] = if chunkSize < 1 then - invalidArg "size" "The input must be positive." + invalidArg "chunkSize" "The input must be positive." let result = ResizeArray<'T[]>() @@ -1133,7 +1189,7 @@ let windowed (windowSize: int) (source: 'T[]) : 'T[][] = let splitInto (chunks: int) (array: 'T[]) : 'T[][] = if chunks < 1 then - invalidArg "chunks" "The input must be positive." + invalidArg "count" "The input must be positive." let result = ResizeArray<'T[]>() @@ -1354,45 +1410,17 @@ let removeAt (index: int) (xs: 'T[]) : 'T[] = ) let removeManyAt (index: int) (count: int) (xs: 'T[]) : 'T[] = - let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 - - let ys = - xs - |> filter (fun _ -> - i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true - ) + // Unlike FSharp.Core, a negative count removes nothing instead of duplicating items + if index < 0 || index > xs.Length - count || index > xs.Length then + invalidArg "index" "index must be within bounds of the array" - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + let mutable i = -1 - ys + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= index + count + ) let updateAt (index: int) (y: 'T) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = let len = xs.Length diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index 3cf4d74945..3d5b14dc1f 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -176,6 +176,9 @@ let inline private newRoot<'T> () : 'T list = let indexNotFound () = raise (System.Collections.Generic.KeyNotFoundException(SR.keyNotFoundAlt)) +let private differentLengths (paramName: string) = + invalidArg paramName "The lists had different lengths." + let cons (x: 'T) (xs: 'T list) = List.Cons(x, xs) let singleton x = List.Cons(x, empty ()) @@ -288,11 +291,29 @@ let fold2<'T1, 'T2, 'State> (folder: 'State -> 'T1 -> 'T2 -> 'State) (state: 'St xs <- xs.Tail ys <- ys.Tail + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc let foldBack2<'T1, 'T2, 'State> (folder: 'T1 -> 'T2 -> 'State -> 'State) (xs: 'T1 list) (ys: 'T2 list) (state: 'State) = // fold2 (fun acc x y -> folder x y acc) state (reverse xs) (reverse ys) - Array.foldBack2 folder (toArray xs) (toArray ys) state + let xs = toArray xs + let ys = toArray ys + + if xs.Length <> ys.Length then + differentLengths ( + if xs.Length = 0 then + "list1" + elif ys.Length = 0 then + "list2" + else + "list1, list2" + ) + + Array.foldBack2 folder xs ys state let unfold (generator: 'State -> ('T * 'State) option) (state: 'State) = let rec loop acc (node: 'T list) = @@ -451,6 +472,11 @@ let map2 (mapping: 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) = let rec loop i (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) = if xs.IsEmpty || ys.IsEmpty then + if not xs.IsEmpty then + differentLengths "list2" + elif not ys.IsEmpty then + differentLengths "list1" + acc else let node = acc.AppendConsNoTail(mapping i xs.Head ys.Head) @@ -464,6 +490,9 @@ let mapIndexed2 (mapping: int -> 'T1 -> 'T2 -> 'U) (xs: 'T1 list) (ys: 'T2 list) let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = let rec loop (acc: 'U list) (xs: 'T1 list) (ys: 'T2 list) (zs: 'T3 list) = if xs.IsEmpty || ys.IsEmpty || zs.IsEmpty then + if not (xs.IsEmpty && ys.IsEmpty && zs.IsEmpty) then + differentLengths "list1, list2, list3" + acc else let node = acc.AppendConsNoTail(mapping xs.Head ys.Head zs.Head) @@ -609,6 +638,9 @@ let contains (value: 'T) (xs: 'T list) ([] eq: System.Collections.Generi tryFindIndex (fun v -> eq.Equals(value, v)) xs |> Option.isSome let initialize n (f: int -> 'T) = + if n < 0 then + invalidArg "count" SR.inputMustBeNonNegative + let root = newRoot () let mutable node = root @@ -634,8 +666,12 @@ let reduceBack f (xs: 'T list) = let forAll f xs = fold (fun acc x -> acc && f x) true xs -let forAll2 f xs ys = - fold2 (fun acc x y -> acc && f x y) true xs ys +let rec forAll2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = + match xs.IsEmpty, ys.IsEmpty with + | true, true -> true + | false, false -> f xs.Head ys.Head && forAll2 f xs.Tail ys.Tail + | true, false -> differentLengths "list1" + | false, true -> differentLengths "list2" let exists f xs = tryFindIndex f xs |> Option.isSome @@ -643,7 +679,7 @@ let rec exists2 (f: 'T1 -> 'T2 -> bool) (xs: 'T1 list) (ys: 'T2 list) = match xs.IsEmpty, ys.IsEmpty with | true, true -> false | false, false -> f xs.Head ys.Head || exists2 f xs.Tail ys.Tail - | _ -> invalidArg "list2" SR.differentLengths + | _ -> differentLengths "list2" let unzip xs = foldBack (fun (x, y) (lacc, racc) -> List.Cons(x, lacc), List.Cons(y, racc)) xs (empty (), empty ()) @@ -726,6 +762,9 @@ let min (xs: 'T list) ([] comparer: System.Collections.Generic.IComparer xs let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let folder acc x = @@ -736,6 +775,9 @@ let average (xs: 'T list) ([] averager: IGenericAverager<'T>) : 'T = averager.DivideByInt(total, count) let averageBy (f: 'T -> 'U) (xs: 'T list) ([] averager: IGenericAverager<'U>) : 'U = + if xs.IsEmpty then + invalidArg "source" SR.inputSequenceEmpty + let mutable count = 0 let inline folder acc x = @@ -972,43 +1014,21 @@ let removeAt (index: int) (xs: 'T list) : 'T list = ys let removeManyAt (index: int) (count: int) (xs: 'T list) : 'T list = + if index < 0 then + invalidArg "index" "index must be within bounds of the list" + + let endIndex = index + count let mutable i = -1 - // incomplete -1, in-progress 0, complete 1 - let mutable status = -1 let ys = xs |> filter (fun _ -> i <- i + 1 - - if i = index then - status <- 0 - false - elif i > index then - if i < index + count then - false - else - status <- 1 - true - else - true + i < index || i >= endIndex ) - let status = - if status = 0 && i + 1 = index + count then - 1 - else - status - - if status < 1 then - // F# always says the wrong parameter is index but the problem may be count - let arg = - if status < 0 then - "index" - else - "count" - - invalidArg arg SR.indexOutOfBounds + if endIndex > i + 1 then + invalidArg "index" "index must be within bounds of the list" ys diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index cf14a251fd..cdad084f7f 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -13,6 +13,7 @@ type IEnumerable<'T> = System.Collections.Generic.IEnumerable<'T> module SR = let enumerationAlreadyFinished = "Enumeration already finished." let enumerationNotStarted = "Enumeration has not started. Call MoveNext." + let inputMustBePositive = "The input must be positive." let inputSequenceEmpty = "The input sequence was empty." let inputSequenceTooLong = "The input sequence contains more than one element." @@ -689,6 +690,9 @@ let head (xs: seq<'T>) = | None -> invalidArg "source" SR.inputSequenceEmpty let initialize count f = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + unfold (fun i -> if (i < count) then @@ -1008,7 +1012,12 @@ let reduceBack folder (xs: seq<'T>) = else invalidArg "source" SR.inputSequenceEmpty -let replicate n x = initialize n (fun _ -> x) +let replicate n x = + // FSharp.Core uses Enumerable.Repeat, which raises ArgumentOutOfRangeException + if n < 0 then + raise (System.ArgumentOutOfRangeException("count")) + + initialize n (fun _ -> x) let reverse (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.rev |> ofArray) @@ -1080,6 +1089,9 @@ let tail (xs: seq<'T>) = ) let take count (xs: seq<'T>) = + if count < 0 then + invalidArg "count" SR.inputMustBeNonNegative + generateIndexed (fun () -> ofSeq xs) (fun i e -> @@ -1128,19 +1140,32 @@ let pairwise (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.pairwise |> ofArray) let splitInto (chunks: int) (xs: seq<'T>) : 'T[] seq = + if chunks < 1 then + invalidArg "count" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.splitInto chunks |> ofArray) let windowed (windowSize: int) (xs: seq<'T>) : 'T[] seq = + if windowSize < 1 then + invalidArg "windowSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.windowed windowSize |> ofArray) let transpose (xss: seq<#seq<'T>>) : seq> = delay (fun () -> - xss - |> toArray - |> Array.map toArray - |> Array.transpose - |> Array.map ofArray - |> ofArray + let rows = ResizeArray>() + + for xs in xss do + let mutable i = 0 + + for x in xs do + if i = rows.Count then + rows.Add(ResizeArray<'T>()) + + rows.[i].Add(x) + i <- i + 1 + + rows |> map (fun row -> row :> seq<'T>) ) let sortWith (comparer: 'T -> 'T -> int) (xs: seq<'T>) = @@ -1244,6 +1269,9 @@ let permute f (xs: seq<'T>) = delay (fun () -> xs |> toArray |> Array.permute f |> ofArray) let chunkBySize (chunkSize: int) (xs: seq<'T>) : seq<'T[]> = + if chunkSize < 1 then + invalidArg "chunkSize" SR.inputMustBePositive + delay (fun () -> xs |> toArray |> Array.chunkBySize chunkSize |> ofArray) // let init = initialize @@ -1348,28 +1376,28 @@ let removeAt (index: int) (xs: seq<'T>) : seq<'T> = let removeManyAt (index: int) (count: int) (xs: seq<'T>) : seq<'T> = if index < 0 then - invalidArg "index" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - generateIndexed - (fun () -> ofSeq xs) - (fun i e -> + let endIndex = index + count + + delay (fun () -> + let mutable i = -1 + + let rest () = if i < index then - if e.MoveNext() then - Some e.Current - else - invalidArg "index" SR.indexOutOfBounds - else - if i = index then - for _ = 1 to count do - if not (e.MoveNext()) then - invalidArg "count" SR.indexOutOfBounds + invalidArg "index" "index must be within bounds of the array" - if e.MoveNext() then - Some e.Current - else - None - ) - (fun e -> e.Dispose()) + empty () + + let ys = + xs + |> filter (fun _ -> + i <- i + 1 + i < index || i >= endIndex + ) + + append ys (delay rest) + ) let updateAt (index: int) (y: 'T) (xs: seq<'T>) : seq<'T> = let mutable isDone = false diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 36cc63eb54..37ba2b0213 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -1506,3 +1506,52 @@ let ``test Array random functions raise ArgumentException for invalid inputs`` ( raisedExceptionType (fun () -> Array.randomSample -1 [|1|]) |> equal "ArgumentException" raisedExceptionType (fun () -> Array.randomSample 3 [|1; 2|]) |> equal "ArgumentException" raisedExceptionType (fun () -> Array.randomSample 1 [||]) |> equal "ArgumentException" + +[] +let ``test Array.sub raises when the range is outside the array`` () = + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 4 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 3 1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|1; 2; 3|] 4 0) |> equal "ArgumentOutOfRangeException" + Array.sub [|1; 2; 3|] 3 0 |> equal [||] + +[] +let ``test Array.exists2 and Array.forall2 check the lengths first`` () = + raisedExceptionType (fun () -> Array.exists2 (fun _ _ -> true) [|1; 2|] [|1; 2; 3|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun _ _ -> false) [|1; 2|] [|1; 2; 3|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.forall2 (fun _ _ -> true) [|1; 2; 3|] [|1; 2|]) |> equal "ArgumentException" + +[] +let ``test Array.chunkBySize and Array.splitInto reject a size below 1`` () = + raisedExceptionType (fun () -> Array.chunkBySize 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.chunkBySize 0 [||]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitInto 0 [|1|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.splitInto -1 [||]) |> equal "ArgumentException" + +[] +let ``test Array.splitInto works with an empty array`` () = + Array.splitInto 3 [||] |> equal [||] + +[] +let ``test Array.transpose raises ArgumentException for arrays of different lengths`` () = + raisedExceptionType (fun () -> Array.transpose [|[|1|]; [|2; 3|]|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [|[|1; 2|]; [|3|]|]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.transpose [|[||]; [|1|]|]) |> equal "ArgumentException" + +[] +let ``test Array.blit raises like System.Array.Copy and leaves the target untouched`` () = + let target = Array.zeroCreate 10 + let blit sourceIndex targetIndex count = + raisedExceptionType (fun () -> Array.blit [|1; 2; 3|] sourceIndex target targetIndex count) + blit 0 0 5 |> equal "ArgumentException" + blit 2 0 2 |> equal "ArgumentException" + blit 4 0 0 |> equal "ArgumentException" + blit 0 9 2 |> equal "ArgumentException" + blit 0 0 -1 |> equal "ArgumentOutOfRangeException" + blit -1 0 2 |> equal "ArgumentOutOfRangeException" + blit 0 -1 2 |> equal "ArgumentOutOfRangeException" + blit 3 0 0 |> equal "no exception" + blit 0 10 0 |> equal "no exception" + target |> equal (Array.zeroCreate 10) diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index 498beca6e1..ddb0027a45 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -940,3 +940,33 @@ let ``test List random functions raise ArgumentException for invalid inputs`` () raisedExceptionType (fun () -> List.randomSample -1 [1]) |> equal "ArgumentException" raisedExceptionType (fun () -> List.randomSample 3 [1; 2]) |> equal "ArgumentException" raisedExceptionType (fun () -> List.randomSample 1 []) |> equal "ArgumentException" + +[] +let ``test List.forall2 and List.exists2 stop once the result is known`` () = + List.forall2 (fun _ _ -> false) [1; 2] [1; 2; 3] |> equal false + List.exists2 (fun _ _ -> true) [1; 2] [1; 2; 3] |> equal true + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) [1; 2; 3] [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exists2 (fun _ _ -> false) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.exists2 (fun _ _ -> false) [1] []) |> equal "ArgumentException" + +[] +let ``test List.chunkBySize, List.windowed and List.splitInto reject a size below 1`` () = + raisedExceptionType (fun () -> List.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.chunkBySize -1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.windowed -1 []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto -4 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.splitInto -1 []) |> equal "ArgumentException" + +[] +let ``test List.splitInto works with an empty list`` () = + List.splitInto 1 [] |> equal [] + List.splitInto 3 [] |> equal [] + +[] +let ``test List.transpose raises ArgumentException for lists of different lengths`` () = + raisedExceptionType (fun () -> List.transpose [[1]; [2; 3]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[1; 2]; [3]]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.transpose [[]; [1]]) |> equal "ArgumentException" diff --git a/tests/Beam/SeqTests.fs b/tests/Beam/SeqTests.fs index ea637949d3..76237f65d1 100644 --- a/tests/Beam/SeqTests.fs +++ b/tests/Beam/SeqTests.fs @@ -1195,3 +1195,39 @@ let ``test Range expressions raise ArgumentException for a zero step`` () = raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" raisedExceptionType (fun () -> [1L .. 0L .. 5L]) |> equal "ArgumentException" raisedExceptionType (fun () -> [1.0 .. 0.0 .. 5.0]) |> equal "ArgumentException" + +[] +let ``test Seq.init, Seq.replicate and Seq.take reject a negative count when called`` () = + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 0) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + +[] +let ``test Seq.chunkBySize, Seq.windowed and Seq.splitInto reject a size below 1 when called`` () = + raisedExceptionType (fun () -> Seq.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed -1 Seq.empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto -1 Seq.empty) |> equal "ArgumentException" + +[] +let ``test Seq.splitInto works with an empty sequence`` () = + Seq.splitInto 2 Seq.empty |> Seq.toList |> equal [] + +[] +let ``test Seq.removeManyAt raises only when the sequence is too short for index`` () = + raisedExceptionType (fun () -> Seq.removeManyAt -1 1 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [1; 2; 3] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + Seq.removeManyAt 1 3 [1; 2; 3] |> Seq.toList |> equal [1] + Seq.removeManyAt 2 4 [1; 2; 3] |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 0 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 0 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + +[] +let ``test Seq.transpose groups jagged input by index`` () = + [[1; 2]; [3]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 3]; [2]] + [|[|1|]; [|2; 3|]|] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 2]; [3]] + [[1; 2; 3]; [4]; [5; 6]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 4; 5]; [2; 6]; [3]] + [[]; [1]] |> Seq.transpose |> Seq.map Seq.toList |> Seq.toList |> equal [[1]] diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index b0284149ab..53ce577dc9 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -1505,6 +1505,48 @@ let tests () = raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 1 [|1|]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + testCase "Array.sub with an index or count out of range raises" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.sub xs -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub xs 4 0) |> equal "ArgumentOutOfRangeException" + Array.sub xs 3 0 |> equal [||] + + testCase "Array.fill with an index or count out of range raises" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.fill xs -1 2 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 0 -1 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 3 1 0) |> equal "IndexOutOfRangeException" + Array.fill xs 4 0 0 + xs |> equal [|1; 2; 3|] + + testCase "Array.blit and System.Array.Copy with an index or count out of range raise" <| fun () -> + let source = [|1; 2; 3|] + let target = Array.zeroCreate 5 + raisedExceptionType (fun () -> Array.blit source 0 target 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source -1 target 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source 0 target -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit source 2 target 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit source 0 target 4 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit source 2 target -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(source, 0, target, 0, 4)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(source, target, -1)) |> equal "ArgumentOutOfRangeException" + target |> equal [|0; 0; 0; 0; 0|] + Array.blit source 3 target 5 0 + target |> equal [|0; 0; 0; 0; 0|] + + testCase "Array.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 4 0 xs) |> equal "ArgumentException" + Array.removeManyAt 3 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 1 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 0 0 ([||]: int[]) |> equal [||] + Array.removeManyAt 1 2 xs |> equal [|1|] + // testCase "System.Array.Resize works" <| fun () -> // let mutable xs = [|1; 2; 3; 4; 5|] // System.Array.Resize(&xs, 3) diff --git a/tests/Dart/src/ListTests.fs b/tests/Dart/src/ListTests.fs index acdde1fa7d..09a58fc102 100644 --- a/tests/Dart/src/ListTests.fs +++ b/tests/Dart/src/ListTests.fs @@ -1106,3 +1106,51 @@ let tests() = raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 1 [1]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions on lists of different lengths raise ArgumentException" <| fun () -> + let xs = [1; 2] + let ys = [1; 2; 3] + raisedExceptionType (fun () -> List.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 ys ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) ys xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) [1] []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) xs ys) |> equal "ArgumentException" + + testCase "List functions on lists of different lengths call the function on the common prefix first" <| fun () -> + let calls = ResizeArray() + raisedExceptionType (fun () -> List.iter2 (fun x y -> calls.Add(x + y)) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> calls.Add(x + y + z)) [1; 2; 3] [1; 2] [1; 2]) |> equal "ArgumentException" + calls |> Seq.toList |> equal [2; 4; 3; 6] + + testCase "List.forall2 returns false before reaching the end of lists of different lengths" <| fun () -> + List.forall2 (fun x y -> x + y < 3) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun _ _ -> false) [1; 2; 3] [1; 2] |> equal false + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> false) [] [1]) |> equal "ArgumentException" + + testCase "List.init and List.replicate with a negative count raise ArgumentException" <| fun () -> + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal [] + + testCase "List.average and List.averageBy on an empty list raise ArgumentException" <| fun () -> + let xs: float list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + + testCase "List.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 2 -1 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 ([]: int list) |> equal [] + List.removeManyAt 1 2 xs |> equal [1] diff --git a/tests/Dart/src/SeqTests.fs b/tests/Dart/src/SeqTests.fs index 795c9e297e..f59e0f0aaa 100644 --- a/tests/Dart/src/SeqTests.fs +++ b/tests/Dart/src/SeqTests.fs @@ -1196,3 +1196,41 @@ let tests() = raisedExceptionType (fun () -> [1 .. 0 .. 5]) |> equal "ArgumentException" raisedExceptionType (fun () -> [|1L .. 0L .. 5L|]) |> equal "ArgumentException" raisedExceptionType (fun () -> [1. .. 0. .. 5.]) |> equal "ArgumentException" + + testCase "Seq.init, Seq.replicate and Seq.take with a negative count raise when called" <| fun () -> + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 (seq { 1; 2 })) |> equal "ArgumentException" + + testCase "Seq.chunkBySize, Seq.windowed and Seq.splitInto check their size when called" <| fun () -> + raisedExceptionType (fun () -> Seq.chunkBySize 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 Seq.empty) |> equal "ArgumentException" + + testCase "Seq.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + let yielded = ResizeArray() + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 xs |> Seq.iter (fun x -> yielded.Add x)) |> equal "ArgumentException" + yielded |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 2 2 xs |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 1 -1 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 1 xs |> Seq.toList |> equal [1; 3] + + testCase "Seq.transpose groups the elements of sequences of different lengths by index" <| fun () -> + let toLists (xss: seq>) = xss |> Seq.map Seq.toList |> Seq.toList + Seq.transpose [[1; 2]; [3]] |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [[1]; [2; 3]] |> toLists |> equal [[1; 2]; [3]] + Seq.transpose [[]; [1]] |> toLists |> equal [[1]] + Seq.transpose [[1; 2; 3]; [4]; [5; 6]] |> toLists |> equal [[1; 4; 5]; [2; 6]; [3]] + Seq.transpose (seq [seq { 1; 2 }; seq { 3 }]) |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [|[|1|]; [||]|] |> toLists |> equal [[1]] + Seq.transpose ([[]]: int list list) |> toLists |> equal [] + let enumerated = ResizeArray() + let xss = Seq.transpose (seq { enumerated.Add 1; yield [1; 2]; yield [3] }) + enumerated.Count |> equal 0 + xss |> toLists |> equal [[1; 3]; [2]] diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index d821dfb7b0..db6751fa4d 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -32,6 +32,7 @@ let raisedExceptionType (f: unit -> 'a): string = | :? System.ArgumentException -> "ArgumentException" | :? System.InvalidOperationException -> "InvalidOperationException" | :? System.Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? System.IndexOutOfRangeException -> "IndexOutOfRangeException" | e -> "Unexpected exception: " + e.Message let private run (f: unit -> 'a) = diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 33b77aebc7..dad65d4280 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -412,6 +412,14 @@ let tests = Array.blit xs 3 ys 5 4 ys.[5] + ys.[6] + ys.[7] + ys.[8] |> equal "defg" + testCase "Array.blit works with overlapping ranges in the same array" <| fun () -> + let xs = [| "a"; "b"; "c"; "d"; "e" |] + Array.blit xs 0 xs 1 3 + xs |> equal [| "a"; "a"; "b"; "c"; "e" |] + let ys = [| "a"; "b"; "c"; "d"; "e" |] + Array.blit ys 1 ys 0 3 + ys |> equal [| "b"; "c"; "d"; "d"; "e" |] + #if FABLE_COMPILER testCase "Array.blit works with deceiving typed arrays" <| fun () -> let xs = Fable.Core.JsInterop.emitJsExpr () "[1,2,3,4,5,6,7,8,9,10]" @@ -1392,6 +1400,14 @@ let tests = System.Array.Copy(source, 1, destination, 1, 2) equal [| "a"; "xx"; "xyz" |] destination + testCase "System.Array.Copy works with Int64 indexes and length" <| fun () -> + let source = [| "xy"; "xx"; "xyz" |] + let destination = [| "a"; "b"; "c"; "d" |] + System.Array.Copy(source, destination, 3L) + equal [| "xy"; "xx"; "xyz"; "d" |] destination + System.Array.Copy(source, 1L, destination, 2L, 2L) + equal [| "xy"; "xx"; "xx"; "xyz" |] destination + testCase "Array.splitInto works" <| fun () -> [|1..10|] |> Array.splitInto 3 |> equal [| [|1..4|]; [|5..7|]; [|8..10|] |] [|1..11|] |> Array.splitInto 3 |> equal [| [|1..4|]; [|5..8|]; [|9..11|] |] @@ -1772,4 +1788,73 @@ let tests = raisedExceptionType (fun () -> Array.randomChoicesBy (fun () -> 1.0) 2 [|1; 2|]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + testCase "Array.sub raises FSharp.Core's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.sub xs -1 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 1 -1) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.sub xs 2 5) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub xs 4 0) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.sub [|"a"; "b"; "c"|] 3 1) |> equal "ArgumentOutOfRangeException" + Array.sub xs 3 0 |> equal [||] + + testCase "Array.fill raises FSharp.Core's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + raisedExceptionType (fun () -> Array.fill xs -1 2 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 0 -1 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.fill xs 3 1 0) |> equal "IndexOutOfRangeException" + raisedExceptionType (fun () -> Array.fill [|"a"; "b"; "c"|] 2 5 "z") |> equal "IndexOutOfRangeException" + xs |> equal [|1; 2; 3|] + Array.fill xs 4 0 0 + xs |> equal [|1; 2; 3|] + + testCase "Array.blit raises System.Array.Copy's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + let ys: int[] = Array.zeroCreate 5 + raisedExceptionType (fun () -> Array.blit xs 0 ys 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs -1 ys 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs 0 ys -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit xs 2 ys 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit xs 0 ys 4 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit xs 4 ys 0 0) |> equal "ArgumentException" + ys |> equal [|0; 0; 0; 0; 0|] + let zs = [|"a"; "b"; "c"|] + let ws = Array.create 5 "_" + raisedExceptionType (fun () -> Array.blit zs 0 ws 0 -1) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs -1 ws 0 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs 0 ws -1 2) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Array.blit zs 2 ws 0 2) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.blit zs 0 ws 4 2) |> equal "ArgumentException" + ws |> equal [|"_"; "_"; "_"; "_"; "_"|] + Array.blit zs 3 ws 5 0 + Array.blit zs 1 ws 3 2 + ws |> equal [|"_"; "_"; "_"; "b"; "c"|] + + testCase "System.Array.Copy raises .NET's exception types for out of range arguments" <| fun () -> + let xs = [|1; 2; 3|] + let ys: int[] = Array.zeroCreate 5 + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, 0, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, -1, ys, 0, 2)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, -1, 2)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 2, ys, 0, 2)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(xs, 0, ys, 4, 2)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy(xs, ys, 4)) |> equal "ArgumentException" + raisedExceptionType (fun () -> System.Array.Copy([|"a"; "b"|], [|"c"|], 2)) |> equal "ArgumentException" + ys |> equal [|0; 0; 0; 0; 0|] + + testCase "Array.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [|1; 2; 3|] + let empty: int[] = [||] + raisedExceptionType (fun () -> Array.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 1 3 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 3 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Array.removeManyAt 0 1 empty) |> equal "ArgumentException" + Array.removeManyAt 0 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 1 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 3 0 xs |> equal [|1; 2; 3|] + Array.removeManyAt 0 0 empty |> equal [||] + Array.removeManyAt 1 2 xs |> equal [|1|] + Array.removeManyAt 0 2 [|"a"; "b"; "c"|] |> equal [|"c"|] + ] diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index 4df3113faf..8082564c81 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1112,4 +1112,63 @@ let tests = raisedExceptionType (fun () -> List.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> List.randomChoicesBy (fun () -> 1.0) 2 [1; 2]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2]) |> equal "ArgumentOutOfRangeException" + + testCase "List functions raise ArgumentException for lists of different lengths" <| fun () -> + let xs = [1; 2] + let ys = [1; 2; 3] + let empty: int list = [] + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) empty xs 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x y z -> x + y + z) ys ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip empty xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip xs empty) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 xs ys ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun x y -> x = y) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun x y -> x = y) empty xs) |> equal "ArgumentException" + + testCase "List functions call the function on the common items of lists of different lengths before raising" <| fun () -> + let mutable calls = 0 + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> calls <- calls + 1) [1; 2] [1; 2; 3]) |> equal "ArgumentException" + equal 2 calls + raisedExceptionType (fun () -> List.map3 (fun x _ _ -> calls <- calls + 1; x) [1; 2; 3] [1; 2] [1; 2; 3]) |> equal "ArgumentException" + equal 4 calls + + testCase "List.forall2 returns false before reaching the end of lists of different lengths" <| fun () -> + List.forall2 (fun x y -> x + y < 3) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun _ _ -> false) [1; 2; 3] [1; 2] |> equal false + + testCase "List.init and List.replicate raise ArgumentException for negative counts" <| fun () -> + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal [] + + testCase "List.average and List.averageBy raise ArgumentException on empty lists" <| fun () -> + let xs: float list = [] + let ys: decimal list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.average ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy id ys) |> equal "ArgumentException" + + testCase "List.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = [1; 2; 3] + let empty: int list = [] + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 3 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 0 1 empty) |> equal "ArgumentException" + List.removeManyAt 0 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 -1 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 empty |> equal [] + List.removeManyAt 1 2 xs |> equal [1] ] diff --git a/tests/Js/Main/SeqTests.fs b/tests/Js/Main/SeqTests.fs index fc19f494ba..418eec38c9 100644 --- a/tests/Js/Main/SeqTests.fs +++ b/tests/Js/Main/SeqTests.fs @@ -1190,4 +1190,41 @@ let tests = raisedExceptionType (fun () -> Seq.permute (fun _ -> 0) [1; 2] |> Seq.toList) |> equal "ArgumentException" raisedExceptionType (fun () -> Seq.randomChoiceBy (fun () -> 1.0) [1; 2]) |> equal "ArgumentOutOfRangeException" raisedExceptionType (fun () -> Seq.randomShuffleBy (fun () -> 1.0) [1; 2] |> Seq.toList) |> equal "ArgumentOutOfRangeException" + + testCase "Seq functions check their arguments when called, before enumerating" <| fun () -> + let xs = seq { for x in [1; 2; 3] do yield x } + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + Seq.init 0 id |> Seq.toList |> equal [] + Seq.take 0 xs |> Seq.toList |> equal [] + + testCase "Seq.removeManyAt raises ArgumentException only when FSharp.Core does" <| fun () -> + let xs = seq { for x in [1; 2; 3] do yield x } + let empty: int seq = Seq.empty + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 xs |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [|1; 2; 3|] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 4 1 xs |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 empty |> Seq.toList) |> equal "ArgumentException" + Seq.removeManyAt 0 4 xs |> Seq.toList |> equal [] + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 1 0 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 2 2 [|1; 2; 3|] |> Seq.toList |> equal [1; 2] + + testCase "Seq.transpose groups the items of jagged sequences by index" <| fun () -> + let transpose (xss: seq>) = Seq.transpose xss |> Seq.map Seq.toList |> Seq.toList + transpose [seq [1; 2]; seq [3]] |> equal [[1; 3]; [2]] + transpose [seq [1]; seq [2; 3]] |> equal [[1; 2]; [3]] + transpose [Seq.empty; seq [1]] |> equal [[1]] + transpose [seq [1]; Seq.empty] |> equal [[1]] + transpose [seq [1; 2; 3]; seq [4]; seq [5; 6]] |> equal [[1; 4; 5]; [2; 6]; [3]] + transpose (seq { for xs in [[1; 2]; [3]] do yield seq { for x in xs do yield x } }) |> equal [[1; 3]; [2]] + Seq.transpose [[1; 2]; [3]] |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 3]; [2]] + Seq.transpose [| [|1|]; [|2; 3|] |] |> Seq.map Seq.toList |> Seq.toList |> equal [[1; 2]; [3]] ] diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index e0c24a8220..647f6c0c39 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -119,6 +119,7 @@ let raisedExceptionType (f: unit -> 'a): string = | :? InvalidOperationException -> "InvalidOperationException" | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" | :? NotSupportedException -> "NotSupportedException" + | :? IndexOutOfRangeException -> "IndexOutOfRangeException" | e -> "Unexpected exception: " + e.Message let rec sumFirstSeq (zs: seq) (n: int): float = diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs index c7db8d1859..c8631a6af8 100644 --- a/tests/Python/TestList.fs +++ b/tests/Python/TestList.fs @@ -1286,3 +1286,73 @@ let ``test List.take and List.splitAt raise InvalidOperationException when the l [] let ``test List.skip raises ArgumentOutOfRangeException when the list is too short`` () = raisedExceptionType (fun () -> List.skip 5 [1]) |> equal "ArgumentOutOfRangeException" + +[] +let ``test List functions walking two lists raise ArgumentException when the lengths differ`` () = + let xs = [1; 2] + let ys = [1; 2; 3] + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.fold2 (fun acc x y -> acc + x + y) 0 ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.foldBack2 (fun x y acc -> x + y + acc) [1] [1; 2] 0) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map2 (+) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.mapi2 (fun i x y -> i + x + y) ys xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iter2 (fun _ _ -> ()) [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.iteri2 (fun _ _ _ -> ()) [1] []) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip ([]: int list) [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> true) xs ys) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.forall2 (fun _ _ -> false) [1] []) |> equal "ArgumentException" + +[] +let ``test List.map3 and List.zip3 raise ArgumentException when the lengths differ`` () = + let add3 x y z = x + y + z + raisedExceptionType (fun () -> List.map3 add3 [1; 2] [1; 2; 3] [1; 2; 3]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 add3 [1; 2; 3] [1; 2; 3] [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 [1] [] [1]) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.zip3 [1; 2] [1; 2] [1; 2; 3]) |> equal "ArgumentException" + +[] +let ``test List functions walking lists in step call back on the common prefix before raising`` () = + let calls = ResizeArray() + raisedExceptionType (fun () -> List.iter2 (fun x _ -> calls.Add x) [1; 2] [1; 2; 3]) + |> equal "ArgumentException" + raisedExceptionType (fun () -> List.map3 (fun x _ _ -> calls.Add x) [3; 4; 5] [1; 2] [1; 2; 3]) + |> equal "ArgumentException" + List.ofSeq calls |> equal [1; 2; 3; 4] + +[] +let ``test List.forall2 stops at the first false before checking the lengths`` () = + List.forall2 (fun _ _ -> false) [1; 2] [1; 2; 3] |> equal false + List.forall2 (fun x _ -> x < 2) [1; 2; 3] [1; 2] |> equal false + List.forall2 (fun x y -> x = y) [1; 2] [1; 2] |> equal true + +[] +let ``test List.init and List.replicate raise ArgumentException for a negative count`` () = + raisedExceptionType (fun () -> List.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.replicate -1 "a") |> equal "ArgumentException" + List.init 0 id |> equal ([]: int list) + List.replicate 0 "a" |> equal [] + +[] +let ``test List.average and List.averageBy raise ArgumentException on empty input`` () = + let xs: float list = [] + raisedExceptionType (fun () -> List.average xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.averageBy (fun x -> x * 2.) xs) |> equal "ArgumentException" + +[] +let ``test List.removeManyAt raises ArgumentException where FSharp.Core does`` () = + let xs = [1; 2; 3] + raisedExceptionType (fun () -> List.removeManyAt -1 1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 2 2 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 4 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> List.removeManyAt 0 1 ([]: int list)) |> equal "ArgumentException" + +[] +let ``test List.removeManyAt works and removes nothing for a count that is not positive`` () = + let xs = [1; 2; 3] + List.removeManyAt 1 2 xs |> equal [1] + List.removeManyAt 1 0 xs |> equal [1; 2; 3] + List.removeManyAt 1 -1 xs |> equal [1; 2; 3] + List.removeManyAt 3 0 xs |> equal [1; 2; 3] + List.removeManyAt 0 0 ([]: int list) |> equal [] diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs index 78314876de..ecacf82993 100644 --- a/tests/Python/TestSeq.fs +++ b/tests/Python/TestSeq.fs @@ -1358,3 +1358,55 @@ let ``test Seq.head and Seq.item raise ArgumentException when the sequence is to raisedExceptionType (fun () -> Seq.item 5 [1]) |> equal "ArgumentException" raisedExceptionType (fun () -> Seq.item 5 [|1|]) |> equal "ArgumentException" raisedExceptionType (fun () -> Seq.item -1 (seq { 1 })) |> equal "ArgumentException" + +[] +let ``test Seq.init, Seq.replicate and Seq.take check for a negative count when called`` () = + raisedExceptionType (fun () -> Seq.init -1 id) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.replicate -1 "a") |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Seq.take -1 (seq { 1; 2 })) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [1; 2]) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.take -1 [|1; 2|]) |> equal "ArgumentException" + Seq.init 0 id |> Seq.toList |> equal [] + Seq.replicate 0 "a" |> Seq.toList |> equal [] + Seq.take 0 (seq { 1; 2 }) |> Seq.toList |> equal [] + +[] +let ``test Seq.chunkBySize, Seq.windowed and Seq.splitInto check the size when called`` () = + let xs = seq { 1; 2; 3 } + raisedExceptionType (fun () -> Seq.chunkBySize 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.chunkBySize -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.windowed -1 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.splitInto -1 xs) |> equal "ArgumentException" + +[] +let ``test Seq.transpose groups jagged input by index`` () = + let toLists (xss: seq>) = xss |> Seq.map Seq.toList |> Seq.toList + Seq.transpose [[1; 2]; [3]] |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [[1]; [2; 3]] |> toLists |> equal [[1; 2]; [3]] + Seq.transpose [[]; [1]] |> toLists |> equal [[1]] + Seq.transpose [[1]; []] |> toLists |> equal [[1]] + Seq.transpose [[1; 2; 3]; [4]; [5; 6]] |> toLists |> equal [[1; 4; 5]; [2; 6]; [3]] + Seq.transpose (seq [seq { 1; 2 }; seq { 3 }]) |> toLists |> equal [[1; 3]; [2]] + Seq.transpose [|[|1; 2|]; [|3|]|] |> toLists |> equal [[1; 3]; [2]] + +[] +let ``test Seq.removeManyAt raises ArgumentException where FSharp.Core does`` () = + let xs = seq { 1; 2; 3 } + raisedExceptionType (fun () -> Seq.removeManyAt -1 0 xs) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 0 0 Seq.empty |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [1; 2; 3] |> Seq.toList) |> equal "ArgumentException" + raisedExceptionType (fun () -> Seq.removeManyAt 3 0 [|1; 2; 3|] |> Seq.toList) |> equal "ArgumentException" + let yielded = ResizeArray() + raisedExceptionType (fun () -> Seq.removeManyAt 4 1 xs |> Seq.iter yielded.Add) |> equal "ArgumentException" + List.ofSeq yielded |> equal [1; 2; 3] + +[] +let ``test Seq.removeManyAt does not raise when the count runs past the end or is not positive`` () = + let xs = seq { 1; 2; 3 } + Seq.removeManyAt 1 3 xs |> Seq.toList |> equal [1] + Seq.removeManyAt 0 4 xs |> Seq.toList |> equal [] + Seq.removeManyAt 2 2 [|1; 2; 3|] |> Seq.toList |> equal [1; 2] + Seq.removeManyAt 1 0 xs |> Seq.toList |> equal [1; 2; 3] + Seq.removeManyAt 1 -1 [1; 2; 3] |> Seq.toList |> equal [1; 2; 3] From 2397032b132efd801b5ea561eadfff6d6b46a94a Mon Sep 17 00:00:00 2001 From: Goswin Date: Wed, 28 Oct 2026 21:14:19 +0100 Subject: [PATCH 262/322] test(js): run the .NET tests with the invariant culture (#5210) Co-authored-by: Claude Opus 5.5 Co-authored-by: Mangel Maxime --- tests/Js/Main/Main.fs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index bcf67edab3..f957ec1b76 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -92,10 +92,15 @@ run() #else +open System.Globalization open Expecto [] let main args = + // Fable formats with the invariant culture, so the .NET run must too + CultureInfo.DefaultThreadCurrentCulture <- CultureInfo.InvariantCulture + CultureInfo.CurrentCulture <- CultureInfo.InvariantCulture + Array.toList allTests |> testList "All" |> runTestsWithCLIArgs [] args From 217ce4a2ec126f862e314c22b0b49d2ac5dc4b04 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 28 Oct 2026 21:16:17 +0100 Subject: [PATCH 263/322] test(js/ts): cover behaviors changed by the perf PRs (#5216) --- tests/Js/Main/ApplicativeTests.fs | 7 +++++++ tests/Js/Main/ArrayTests.fs | 9 +++++++++ tests/Js/Main/ComparisonTests.fs | 6 ++++++ tests/Js/Main/DictionaryTests.fs | 20 ++++++++++++++++++++ tests/Js/Main/EnumerableTests.fs | 14 +++++++++++++- tests/Js/Main/HashSetTests.fs | 17 +++++++++++++++++ tests/Js/Main/ListTests.fs | 21 +++++++++++++++++++++ tests/Js/Main/MiscTests.fs | 9 +++++++++ 8 files changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/Js/Main/ApplicativeTests.fs b/tests/Js/Main/ApplicativeTests.fs index 867f7ea897..4916bed08e 100644 --- a/tests/Js/Main/ApplicativeTests.fs +++ b/tests/Js/Main/ApplicativeTests.fs @@ -2,6 +2,7 @@ module Fable.Tests.Applicative open System open Util.Testing +open Fable.Tests.Util let inline (|HasLength|) x = fun () -> (^a: (member Length: int) x) @@ -1190,6 +1191,12 @@ let tests7 = [ apply3 r.myFunction 5 7 4 |> equal 8 apply (r.myFunction 1 1 |> Some) (Some 5) |> equal (Some -3) + testCase "Null functions in record fields can be read back" <| fun () -> + let r = { myFunction = Unchecked.defaultof<_> } + let f = r.myFunction + throwsAnyError (fun () -> f 1 2 3) + throwsAnyError (fun () -> r.myFunction 1 2 3) + testCase "Uncurried functions in record fields can be partially applied" <| fun () -> // Test also non-record functions just in case let result = getStrLen () "hello" diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index dad65d4280..ca205b7f76 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -271,6 +271,15 @@ let tests = let b = [| |] throwsAnyError (fun () -> b.[0]) + testCase "Array getter returns None for an in-bounds option element" <| fun () -> + let xs = [| Some 1; None; Some 3 |] + Array.get xs 1 |> equal None + xs[1] |> equal None + xs.[1] |> equal None + Array.item 1 xs |> equal None + xs[2] |> equal (Some 3) + throwsAnyError (fun () -> xs[3]) + testCase "Array setter works" <| fun () -> let x = [| 1.; 2.; 3.; 4.; 5. |] Array.set x 3 10. diff --git a/tests/Js/Main/ComparisonTests.fs b/tests/Js/Main/ComparisonTests.fs index 39ee5d9cf3..0a165d26b2 100644 --- a/tests/Js/Main/ComparisonTests.fs +++ b/tests/Js/Main/ComparisonTests.fs @@ -551,6 +551,12 @@ let tests = let s2: String = "hello" isNull s2 |> equal false +#if FABLE_COMPILER + testCase "isNull treats undefined as null" <| fun () -> + let s: String = unbox Fable.Core.JS.undefined + isNull s |> equal true +#endif + testCase "Classes must use identity hashing by default" <| fun () -> // See #2291 let x = MyClass(5) let y = MyClass(5) diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index a3a86a9340..18355832d5 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -12,6 +12,10 @@ type MyRecord = { a: int } type R = { i: int; s: string } +type DictKey = + | Int of int + | Str of string + type Table<'K, 'V when 'K: equality and 'K: not null> () = let dic = new Dictionary<'K,'V>() @@ -228,6 +232,22 @@ let tests = dic.Count |> equal 1 dic.["A"] |> equal 2 + testCase "Dictionary works with union keys" <| fun () -> + let dic = Dictionary() + dic.Add(Int 1, 1) + dic.Add(Str "a", 2) + dic.[Int 1] |> equal 1 + dic.[Str "a"] |> equal 2 + dic.ContainsKey(Int 1) |> equal true + dic.ContainsKey(Int 2) |> equal false + dic.ContainsKey(Str "b") |> equal false + dic.[Int 1] <- 3 + dic.[Int 1] |> equal 3 + dic.Count |> equal 2 + dic.Remove(Str "a") |> equal true + dic.ContainsKey(Str "a") |> equal false + dic.Count |> equal 1 + testCase "Dictionary.Remove with complex keys works" <| fun () -> let dic = Dictionary<_, _>() dic.Add((0,"A"), "Hello") diff --git a/tests/Js/Main/EnumerableTests.fs b/tests/Js/Main/EnumerableTests.fs index ff1339c5f7..478e9ad68e 100644 --- a/tests/Js/Main/EnumerableTests.fs +++ b/tests/Js/Main/EnumerableTests.fs @@ -66,4 +66,16 @@ let tests = |> Seq.toList |> List.sum |> equal 55 - ] \ No newline at end of file + + testCase "Array enumerator can be reset" <| fun () -> + let e = ([| 1; 2; 3 |] :> seq).GetEnumerator() + e.MoveNext() |> equal true + e.MoveNext() |> equal true + e.Current |> equal 2 + e.Reset() + e.MoveNext() |> equal true + e.Current |> equal 1 + e.MoveNext() |> equal true + e.MoveNext() |> equal true + e.MoveNext() |> equal false + ] diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index ff8953b1bb..0ee52339b6 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -32,6 +32,10 @@ type MyRecord = { a: int } type R = { i: int; s: string } +type SetItem = + | Int of int + | Str of string + type Apa<'t when 't : equality>() = let state = HashSet<'t>() member _.Add t = state.Add t |> ignore @@ -107,6 +111,19 @@ let tests = xs.Contains "C" |> equal true xs.Count |> equal 2 + testCase "HashSet works with union items" <| fun () -> + let xs = HashSet() + xs.Add(Int 1) |> equal true + xs.Add(Str "a") |> equal true + xs.Add(Int 1) |> equal false + xs.Contains(Int 1) |> equal true + xs.Contains(Int 2) |> equal false + xs.Contains(Str "a") |> equal true + xs.Count |> equal 2 + xs.Remove(Int 1) |> equal true + xs.Contains(Int 1) |> equal false + xs.Count |> equal 1 + testCase "HashSet.UnionWith works" <| fun () -> let xs = hashSet [1; 2] let ys = hashSet [2; 4] diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index 8082564c81..e8f2d8c14a 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1095,6 +1095,27 @@ let tests = raisedExceptionType (fun () -> [1].Tail.Head) |> equal "InvalidOperationException" raisedExceptionType (fun () -> [1].Tail.Tail) |> equal "InvalidOperationException" + testCase "List builders leave the empty list untouched" <| fun () -> + let xs = [1; 2; 3] + List.map ((+) 1) xs |> equal [2; 3; 4] + List.filter (fun x -> x > 1) xs |> equal [2; 3] + List.filter (fun _ -> false) xs |> equal [] + List.choose (fun x -> if x > 2 then Some x else None) xs |> equal [3] + List.collect (fun x -> [x; x]) xs |> equal [1; 1; 2; 2; 3; 3] + List.append xs xs |> equal [1; 2; 3; 1; 2; 3] + List.concat [xs; xs] |> equal [1; 2; 3; 1; 2; 3] + List.ofSeq (seq { 1 .. 3 }) |> equal xs + List.partition (fun x -> x > 1) xs |> equal ([2; 3], [1]) + List.scan (+) 0 xs |> equal [0; 1; 3; 6] + List.take 2 xs |> equal [1; 2] + List.init 3 id |> equal [0; 1; 2] + List.unfold (fun s -> if s < 3 then Some (s, s + 1) else None) 0 |> equal [0; 1; 2] + let empty: int list = [] + List.isEmpty empty |> equal true + List.length empty |> equal 0 + List.isEmpty (List.map ((+) 1) empty) |> equal true + empty |> equal [] + testCase "List functions raise FSharp.Core's exception types for out of range counts" <| fun () -> raisedExceptionType (fun () -> List.take 5 [1]) |> equal "InvalidOperationException" raisedExceptionType (fun () -> List.take -1 [1]) |> equal "ArgumentException" diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 910b403556..6df4c81f2c 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -1278,6 +1278,15 @@ let tests = | e -> "Unexpected exception: " + e.Message equal "This is invalid" caught + testCase "invalidArg and invalidOp work in expression position" <| fun () -> + let pick b = if b then 1 else invalidArg "b" "This is invalid" + pick true |> equal 1 + throwsError "This is invalid (Parameter 'b')" (fun () -> pick false) + let wrap (x: int) = x + 1 + throwsError "This is invalid" (fun () -> wrap (invalidOp "This is invalid")) + Option.defaultWith (fun () -> invalidOp "This is invalid") (Some 3) |> equal 3 + throwsError "This is invalid" (fun () -> Option.defaultWith (fun () -> invalidOp "This is invalid") None) + testCase "Pattern matching optimization works (switch statement)" <| fun () -> let mutable x = "" let i = 4 From 82e606e9f8567d9bdad743df1b5de6abbdda89b0 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Wed, 28 Oct 2026 13:24:01 -0700 Subject: [PATCH 264/322] fix(js/ts): parse numbers like .NET and raise its exception types (#5200) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Replacements.fs | 17 +++- src/fable-library-ts/BigInt.ts | 22 ++-- src/fable-library-ts/Decimal.ts | 21 +++- src/fable-library-ts/Double.ts | 28 +++-- src/fable-library-ts/Int32.ts | 12 ++- src/fable-library-ts/Long.ts | 9 +- tests/Js/Main/ConvertTests.fs | 147 +++++++++++++++++++++++++++ tests/Js/Main/Util/Util.fs | 3 + 8 files changed, 235 insertions(+), 24 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 616d36d473..9c3bbfb6ad 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -268,7 +268,7 @@ let needToCast fromKind toKind = let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr = match args.Head.Type with | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) - | String -> Helper.LibCall(com, "Double", "parse", targetType, args) + | String -> Helper.LibCall(com, "Double", "parse", targetType, [ args.Head ]) | Number(kind, _) -> match kind with | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) @@ -284,7 +284,10 @@ let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr = | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) |> makeDecimalFromExpr com r targetType - | String -> makeDecimalFromExpr com r targetType args.Head + // FSharp.Core's decimal parses with NumberStyles.Float + | String -> + let style = int System.Globalization.NumberStyles.Float + Helper.LibCall(com, "Decimal", "parse", targetType, [ args.Head; makeIntConst style ], ?loc = r) | Number(kind, _) -> match kind with | Decimal -> args.Head @@ -1524,6 +1527,10 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | ("ToSByte" | "ToByte" | "ToInt8" | "ToUInt8" | "ToInt16" | "ToUInt16" | "ToInt" | "ToUInt" | "ToInt32" | "ToUInt32"), _ -> toInt com ctx r t args |> Some | ("ToInt64" | "ToUInt64" | "ToIntPtr" | "ToUIntPtr"), _ -> toLong com ctx r t args |> Some + // Unlike Double.Parse, FSharp.Core's float and float32 remove underscores + | ("ToSingle" | "ToDouble"), [ ExprType String as str ] -> + Helper.LibCall(com, "Double", "parseIgnoringUnderscores", t, [ str ], ?loc = r) + |> Some | ("ToSingle" | "ToDouble"), _ -> toFloat com ctx r t args |> Some | "ToDecimal", _ -> toDecimal com ctx r t args |> Some | "ToChar", _ -> toChar com ctx r args.Head |> Some @@ -3405,7 +3412,11 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( | "ToUInt64" -> round com args |> toLong com ctx r t |> Some | "ToSingle" | "ToDouble" -> toFloat com ctx r t args |> Some - | "ToDecimal" -> toDecimal com ctx r t args |> Some + | "ToDecimal" -> + match args with + // Convert.ToDecimal(string) parses with Decimal.Parse's default style + | ExprType String :: _ -> Helper.LibCall(com, "Decimal", "parse", t, [ args.Head ], ?loc = r) |> Some + | _ -> toDecimal com ctx r t args |> Some | "ToChar" -> toChar com ctx r args.Head |> Some | "ToString" -> toString com ctx r args |> Some | "ToBase64String" diff --git a/src/fable-library-ts/BigInt.ts b/src/fable-library-ts/BigInt.ts index 18539c8231..9593f1328f 100644 --- a/src/fable-library-ts/BigInt.ts +++ b/src/fable-library-ts/BigInt.ts @@ -2,6 +2,7 @@ import { FSharpRef } from "./Types.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; import { decimal, fromParts, truncate } from "./Decimal.ts"; import { Exception, bigintHash } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; const isBigEndian = false; @@ -195,18 +196,27 @@ export function toChar(x: bigint): string { export function toString(x: bigint): string { return x.toString(); } +// .NET's Integer style: white space, a sign and decimal digits, then NULs +const integerRegex = /^[\t-\r ]*([+-]?\d+)[\t-\r ]*\0*$/; + export function tryParse(s: string, res: FSharpRef): boolean { - try { - res.contents = BigInt(s); + const m = s == null ? null : integerRegex.exec(s); + if (m != null) { + res.contents = BigInt(m[1]); return true; } - catch (err: any) { - return false; - } + return false; } export function parse(s: string): bigint { - return BigInt(s); + const res = new FSharpRef(zero); + if (tryParse(s, res)) { + return res.contents; + } else if (s == null) { + throw ArgumentNullException_$ctor_Z721C83C5("value"); + } else { + throw FormatException_$ctor_Z721C83C5("The value could not be parsed."); + } } export function pow(x: bigint, n: int32): bigint { diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index 016398d222..fa8f3a2c54 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -2,6 +2,7 @@ import Decimal, { BigSource } from "./lib/big.js"; import { Numeric, symbol } from "./Numeric.ts"; import { FSharpRef } from "./Types.ts"; import { Exception, combineHashCodes } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } from "./BigInt.ts"; import * as bigInt from "./BigInt.ts"; @@ -159,12 +160,13 @@ function parseWithStyle(str: string, style: number): Decimal | undefined { if (style & NumberStyles.AllowHexSpecifier) { throw new Exception("The number style AllowHexSpecifier is not supported on floating point data types."); } - let s = str; + // .NET ignores trailing NULs and its white space is only [\t\n\v\f\r ] + let s = str.replace(/\0+$/, ""); if (style & NumberStyles.AllowLeadingWhite) { - s = s.replace(/^\s+/, ""); + s = s.replace(/^[\t-\r ]+/, ""); } if (style & NumberStyles.AllowTrailingWhite) { - s = s.replace(/\s+$/, ""); + s = s.replace(/[\t-\r ]+$/, ""); } let negative = false; if (style & NumberStyles.AllowParentheses && s.startsWith("(") && s.endsWith(")")) { @@ -181,7 +183,13 @@ function parseWithStyle(str: string, style: number): Decimal | undefined { if (!digitsPattern(style).test(s)) { return undefined; } - const d = new Decimal(s.replace(/,/g, "")); + let d: Decimal; + try { + d = new Decimal(s.replace(/,/g, "")); + } catch { + // Fable's big.js fork throws for values outside the decimal range + throw OverflowException_$ctor_Z721C83C5(OVERFLOW); + } if (negative) { d.s = -1; } @@ -202,9 +210,12 @@ export function tryParse(str: string, style: number, defValue: FSharpRef): boolean { - // TODO: test if value is valid and in range - if (str != null && /\S/.test(str)) { - const v = +str.replace("_", ""); - if (!Number.isNaN(v)) { - defValue.contents = v; + if (str != null) { + if (floatRegex.test(str)) { + defValue.contents = Number.parseFloat(str); + return true; + } + const symbol = symbolRegex.exec(str); + if (symbol != null) { + defValue.contents = symbol[1] == null ? NaN : symbol[1] === "-" ? -Infinity : Infinity; return true; } } @@ -17,11 +25,17 @@ export function parse(str: string): number { const defValue = new FSharpRef(0); if (tryParse(str, defValue)) { return defValue.contents; + } else if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); } else { - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } } +export function parseIgnoringUnderscores(str: string): number { + return parse(str == null ? str : str.replace(/_/g, "")); +} + // JS Number.isFinite function evals false for NaN export function isPositiveInfinity(x: number) { return x === Number.POSITIVE_INFINITY; diff --git a/src/fable-library-ts/Int32.ts b/src/fable-library-ts/Int32.ts index 743272624a..21220602bc 100644 --- a/src/fable-library-ts/Int32.ts +++ b/src/fable-library-ts/Int32.ts @@ -1,5 +1,6 @@ import { FSharpRef } from "./Types.ts"; import { Exception } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; export type int8 = number; export type uint8 = number; @@ -106,7 +107,15 @@ export function isValid(str: string, style: number, radix?: number) { return null; } +export function parseOverflow(unsigned: boolean, bitsize: number) { + const type = bitsize === 8 ? (unsigned ? "an unsigned byte" : "a signed byte") : (unsigned ? "a UInt" : "an Int") + bitsize; + return OverflowException_$ctor_Z721C83C5(`Value was either too large or too small for ${type}.`); +} + export function parse(str: string, style: number, unsigned: boolean, bitsize: number, radix?: number): number { + if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } const res = isValid(str, style, radix); if (res != null) { let v = Number.parseInt(res.sign + res.digits, res.radix); @@ -119,9 +128,10 @@ export function parse(str: string, style: number, unsigned: boolean, bitsize: nu if (v >= min && v <= max) { return v; } + throw parseOverflow(unsigned, bitsize); } } - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } export function tryParse(str: string, style: number, unsigned: boolean, bitsize: number, defValue: FSharpRef): boolean { diff --git a/src/fable-library-ts/Long.ts b/src/fable-library-ts/Long.ts index ed81356dc6..9b35950809 100644 --- a/src/fable-library-ts/Long.ts +++ b/src/fable-library-ts/Long.ts @@ -1,7 +1,8 @@ -import { getPrefix, isValid } from "./Int32.ts"; +import { getPrefix, isValid, parseOverflow } from "./Int32.ts"; import { fromString } from "./BigInt.ts"; import { FSharpRef } from "./Types.ts"; import { Exception } from "./Util.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; function getRange(unsigned: boolean, bitsize: number): [bigint, bigint] { switch (bitsize) { @@ -13,6 +14,9 @@ function getRange(unsigned: boolean, bitsize: number): [bigint, bigint] { } export function parse(str: string, style: number, unsigned: boolean, bitsize: number, radix?: number) { + if (str == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } const res = isValid(str, style, radix); if (res != null) { let v = fromString(getPrefix(res.radix) + res.digits); @@ -27,8 +31,9 @@ export function parse(str: string, style: number, unsigned: boolean, bitsize: nu if (v >= min && v <= max) { return v; } + throw parseOverflow(unsigned, bitsize); } - throw new Exception(`The input string ${str} was not in a correct format.`); + throw FormatException_$ctor_Z721C83C5(`The input string '${str}' was not in a correct format.`); } export function tryParse(str: string, style: number, unsigned: boolean, bitsize: number, defValue: FSharpRef) { diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 7cd82bdbb9..855dc8c9bf 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -26,6 +26,14 @@ let parse f (a: string) = #endif +// The one-argument overload depends on the current culture on .NET, and the explicit style would trigger FABLE0103 on Fable +let tryParseFloat (s: string) = +#if FABLE_COMPILER + Double.TryParse s +#else + Double.TryParse(s, NumberStyles.Float ||| NumberStyles.AllowThousands, CultureInfo.InvariantCulture) +#endif + let private toPlainArray (bytes: byte[]) : byte[] = #if FABLE_COMPILER ResizeArray bytes |> unbox @@ -89,6 +97,145 @@ let tests = tryParse Single.TryParse 0.0f "9SayWhat12Huh" |> equal (false, 0.0f) tryParse Single.TryParse 0.0f "-1.5" |> equal (true, -1.5f) + testCase "System.Double.Parse follows .NET's Float grammar" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + parse "1." |> equal 1.0 + parse ".5" |> equal 0.5 + parse "+.5" |> equal 0.5 + parse "-1.5e3" |> equal -1500.0 + parse "25E-2" |> equal 0.25 + parse "\t1\n" |> equal 1.0 + parse "\v1\f" |> equal 1.0 + parse "1\u0000" |> equal 1.0 + parse "1 \u0000\u0000" |> equal 1.0 + parse "1e99999" |> equal Double.PositiveInfinity + for s in [ "0x10"; "0X1A"; "0b101"; "0o7"; "\u00a01"; "\ufeff1"; "1\ufeff"; "\u20281"; "\u30001"; "1\u0000 "; "\u00001"; "1e"; "e5"; "."; "+"; "\uff11" ] do + (fun () -> parse s) |> throwsAnyError + + testCase "System.Double.Parse accepts the NaN and infinity symbols ignoring case" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + for s in [ "NaN"; "nan"; "NAN"; "-NaN"; "+nan"; " NaN\n" ] do + parse s |> Double.IsNaN |> equal true + parse "infinity" |> equal Double.PositiveInfinity + parse "+Infinity" |> equal Double.PositiveInfinity + parse " -INFINITY " |> equal Double.NegativeInfinity + parse " NaN" |> Double.IsNaN |> equal true + parse "\u0085Infinity\u2028" |> equal Double.PositiveInfinity + parse "\u3000-Infinity" |> equal Double.NegativeInfinity + for s in [ "inf"; "\u221e"; "NaN\u0000"; "\ufeffNaN"; "NaN\ufeff"; "- NaN"; "+-Infinity"; "Infinityx"; "NaN0" ] do + (fun () -> parse s) |> throwsAnyError + + testCase "System.Double.TryParse follows .NET's Float grammar" <| fun () -> + tryParseFloat "1." |> equal (true, 1.0) + tryParseFloat " .5\r\n" |> equal (true, 0.5) + tryParseFloat "1\u0000" |> equal (true, 1.0) + tryParseFloat "-infinity" |> equal (true, Double.NegativeInfinity) + tryParseFloat "nan" |> fst |> equal true + tryParseFloat "0x10" |> equal (false, 0.0) + tryParseFloat "\u00a01" |> equal (false, 0.0) + tryParseFloat "1_0" |> equal (false, 0.0) + tryParseFloat "_" |> equal (false, 0.0) + tryParseFloat null |> equal (false, 0.0) + + testCase "System.Single.Parse follows .NET's Float grammar" <| fun () -> + let parse (s: string) = Single.Parse(s, CultureInfo.InvariantCulture) + parse ".5" |> equal 0.5f + parse "-Infinity" |> float |> equal Double.NegativeInfinity + parse "nan" |> float |> Double.IsNaN |> equal true + (fun () -> parse "0x10") |> throwsAnyError + (fun () -> parse "1_0") |> throwsAnyError + + testCase "Double.Parse and Convert.ToDouble reject underscores" <| fun () -> + for s in [ "1_0"; "_1"; "1_"; "_"; "1e1_0"; "1._5" ] do + (fun () -> Double.Parse(s, CultureInfo.InvariantCulture)) |> throwsAnyError + (fun () -> Convert.ToDouble(s, CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "float and float32 remove underscores like FSharp.Core" <| fun () -> + float "1_0_0" |> equal 100.0 + float "_1" |> equal 1.0 + float "1_" |> equal 1.0 + float "1e1_0" |> equal 1e10 + float " _1" |> equal 1.0 + float "Na_N" |> Double.IsNaN |> equal true + float32 "1__0" |> equal 10.0f + (fun () -> float "_") |> throwsAnyError + (fun () -> float32 "__") |> throwsAnyError + (fun () -> float "0x10") |> throwsAnyError + (fun () -> float "\u00a01") |> throwsAnyError + + testCase "System.Decimal.Parse trims only .NET's white space" <| fun () -> + let parse (s: string) = Decimal.Parse(s, CultureInfo.InvariantCulture) + parse "\t12\r\n" |> equal 12M + parse "12\u0000" |> equal 12M + parse "12 \u0000" |> equal 12M + for s in [ "\u00a012"; "\ufeff12"; "12\u3000"; "12\u0000 " ] do + (fun () -> parse s) |> throwsAnyError + + testCase "decimal parses like System.Decimal.Parse with NumberStyles.Float" <| fun () -> + decimal " 12 " |> equal 12M + decimal "+12" |> equal 12M + decimal "\t12\n" |> equal 12M + decimal "12\u0000" |> equal 12M + decimal "1e3" |> equal 1000M + for s in [ "1,000"; "(1)"; "1-"; "1_0"; "\u00a012"; "0x10"; "NaN" ] do + (fun () -> decimal s) |> throwsAnyError + + testCase "Convert.ToDecimal parses like System.Decimal.Parse" <| fun () -> + Convert.ToDecimal(" 12 ", CultureInfo.InvariantCulture) |> equal 12M + Convert.ToDecimal("1,000", CultureInfo.InvariantCulture) |> equal 1000M + Convert.ToDecimal("1-", CultureInfo.InvariantCulture) |> equal -1M + (fun () -> Convert.ToDecimal("1e3", CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "BigInteger.Parse follows .NET's Integer grammar" <| fun () -> + bigint.Parse " 12 " |> equal 12I + bigint.Parse "+12" |> equal 12I + bigint.Parse "-12\u0000" |> equal -12I + for s in [ ""; " "; "-"; "0x10"; "1_0"; "1.5"; "1e3"; "\u00a012" ] do + (fun () -> bigint.Parse s) |> throwsAnyError + tryParse bigint.TryParse 0I "" |> equal (false, 0I) + tryParse bigint.TryParse 0I "0x10" |> equal (false, 0I) + tryParse bigint.TryParse 0I " 12 " |> equal (true, 12I) + + testCase "Parsing numbers raises .NET's exception types" <| fun () -> + raisedExceptionType (fun () -> Double.Parse("1e", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> Double.Parse(null, CultureInfo.InvariantCulture)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Single.Parse("abc", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> float "abc") |> equal "FormatException" + raisedExceptionType (fun () -> float32 (null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Int32.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> Int32.Parse(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Int32.Parse "2147483648") |> equal "OverflowException" + raisedExceptionType (fun () -> UInt32.Parse "4294967296") |> equal "OverflowException" + raisedExceptionType (fun () -> Byte.Parse "256") |> equal "OverflowException" + raisedExceptionType (fun () -> SByte.Parse "-129") |> equal "OverflowException" + raisedExceptionType (fun () -> UInt16.Parse "-1") |> equal "OverflowException" + raisedExceptionType (fun () -> Int64.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> Int64.Parse(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> UInt64.Parse "-1") |> equal "OverflowException" + raisedExceptionType (fun () -> int "1.5") |> equal "FormatException" + raisedExceptionType (fun () -> uint32 "4294967296") |> equal "OverflowException" + raisedExceptionType (fun () -> int64 "99999999999999999999") |> equal "OverflowException" + raisedExceptionType (fun () -> Decimal.Parse("abc", CultureInfo.InvariantCulture)) |> equal "FormatException" + raisedExceptionType (fun () -> Decimal.Parse(null, CultureInfo.InvariantCulture)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Decimal.Parse("79228162514264337593543950336", CultureInfo.InvariantCulture)) |> equal "OverflowException" + raisedExceptionType (fun () -> decimal "abc") |> equal "FormatException" + raisedExceptionType (fun () -> decimal "1e30") |> equal "OverflowException" + raisedExceptionType (fun () -> bigint.Parse "abc") |> equal "FormatException" + raisedExceptionType (fun () -> bigint.Parse null) |> equal "ArgumentNullException" + tryParse Int32.TryParse 0 null |> equal (false, 0) + tryParse Decimal.TryParse 0M null |> equal (false, 0M) + + testCase "Parsing numbers raises .NET's exception messages" <| fun () -> + (fun () -> Int32.Parse "abc") |> throwsError "The input string 'abc' was not in a correct format." + (fun () -> Double.Parse("abc", CultureInfo.InvariantCulture)) |> throwsError "The input string 'abc' was not in a correct format." + (fun () -> Int32.Parse(null: string)) |> throwsError "Value cannot be null. (Parameter 's')" + (fun () -> Int32.Parse "2147483648") |> throwsError "Value was either too large or too small for an Int32." + (fun () -> UInt32.Parse "4294967296") |> throwsError "Value was either too large or too small for a UInt32." + (fun () -> Byte.Parse "256") |> throwsError "Value was either too large or too small for an unsigned byte." + (fun () -> SByte.Parse "128") |> throwsError "Value was either too large or too small for a signed byte." + (fun () -> Int64.Parse "9223372036854775808") |> throwsError "Value was either too large or too small for an Int64." + (fun () -> bigint.Parse "abc") |> throwsError "The value could not be parsed." + testCase "System.Boolean.Parse works" <| fun () -> Boolean.Parse "true" |> equal true Boolean.Parse "True" |> equal true diff --git a/tests/Js/Main/Util/Util.fs b/tests/Js/Main/Util/Util.fs index 647f6c0c39..954d89af5f 100644 --- a/tests/Js/Main/Util/Util.fs +++ b/tests/Js/Main/Util/Util.fs @@ -115,7 +115,10 @@ let raisedExceptionType (f: unit -> 'a): string = "no exception" with | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" | :? ArgumentException -> "ArgumentException" + | :? FormatException -> "FormatException" + | :? OverflowException -> "OverflowException" | :? InvalidOperationException -> "InvalidOperationException" | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" | :? NotSupportedException -> "NotSupportedException" From cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b Mon Sep 17 00:00:00 2001 From: Goswin Date: Wed, 28 Oct 2026 22:18:43 +0100 Subject: [PATCH 265/322] perf(js/ts): pass array literals to typed array constructors as plain arrays (#5209) Co-authored-by: Claude Opus 5.5 Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Babel/Fable2Babel.fs | 32 +++++++++++++++++++++++ tests/Js/Main/JsInteropTests.fs | 10 +++++++ 2 files changed, 42 insertions(+) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index d312bbdf57..89cb0d41e1 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -2117,6 +2117,35 @@ module Util = | Fable.Logical(op, TransformExpr com ctx left, TransformExpr com ctx right) -> Expression.logicalExpression (left, op, right, ?loc = range) + let isTypedArrayConstructor (expr: Fable.Expr) = + match expr with + | Fable.IdentExpr ident -> + match ident.Name with + | "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "BigInt64Array" + | "BigUint64Array" -> true + | _ -> false + | _ -> false + + // Typed array constructors copy and convert their argument, so the literal does not need to be a typed array itself + let unwrapTypedArrayConstructorArg (macro: string) (info: Fable.CallInfo) = + match macro, info.ThisArg, info.Args with + | "new $0($1...)", + Some cons, + [ MaybeCasted(Fable.Value(Fable.NewArray(Fable.ArrayValues _ as values, typ, _), r)) ] when + isTypedArrayConstructor cons + -> + { info with Args = [ Fable.Value(Fable.NewArray(values, typ, Fable.ResizeArray), r) ] } + | _ -> info + let transformEmit (com: IBabelCompiler) ctx range (info: Fable.EmitInfo) = match info.CallInfo.Tags with | Fable.Tags.Contains "topDirectiveProloge" -> @@ -2124,6 +2153,9 @@ module Util = // Return empty sequence expression so it can be erased at print time Expression.sequenceExpression ([||]) | _ -> + let info = + { info with CallInfo = unwrapTypedArrayConstructorArg info.Macro info.CallInfo } + let macro = stripImports com ctx range info.Macro let info = info.CallInfo diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 978240b835..7f0fd3b346 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -1109,6 +1109,16 @@ let tests = arr.[0] <- 5uy equal 5uy arr.[0] + testCase "TypedArray can be created from an array literal" <| fun () -> + let floats = JS.Constructors.Float32Array.Create [| 1.5; -2.5 |] + floats.length |> equal 2 + floats.[1] |> equal -2.5f + let bytes = JS.Constructors.Uint8Array.Create [| 1uy; 255uy |] + bytes.[1] |> equal 255uy + // Values are converted the same way as from another typed array + let wrapped = JS.Constructors.Int8Array.Create [| 1; 300 |] + wrapped.[1] |> equal 44y + testCase "jsOptions works" <| fun _ -> let opts = jsOptions(fun o -> o.foo <- "bar" From 82b935f3859b36923e5e0eb26771f860eadbe525 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 28 Oct 2026 22:24:44 +0100 Subject: [PATCH 266/322] fix(js/ts): honor NumberStyles when parsing floats (#5217) --- src/Fable.Transforms/Replacements.fs | 113 ++++++++++-------- src/fable-library-ts/Decimal.ts | 53 +------- src/fable-library-ts/Double.ts | 19 +-- src/fable-library-ts/NumberStyles.ts | 63 ++++++++++ .../Compiler/IgnoredArgumentTests.fs | 12 ++ tests/Js/Main/ConvertTests.fs | 45 ++++++- 6 files changed, 195 insertions(+), 110 deletions(-) create mode 100644 src/fable-library-ts/NumberStyles.ts diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 9c3bbfb6ad..bb1331fb10 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -268,7 +268,14 @@ let needToCast fromKind toKind = let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr = match args.Head.Type with | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) - | String -> Helper.LibCall(com, "Double", "parse", targetType, [ args.Head ]) + | String -> + let style = + int ( + System.Globalization.NumberStyles.Float + ||| System.Globalization.NumberStyles.AllowThousands + ) + + Helper.LibCall(com, "Double", "parse", targetType, [ args.Head; makeIntConst style ]) | Number(kind, _) -> match kind with | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) @@ -2763,7 +2770,7 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let parseCall meth str args style = + let parseCall meth str args (style: Expr) = let kind = match i.DeclaringEntityFullName with | Patterns.DicContains FSharp2Fable.TypeHelpers.numberTypes kind -> kind @@ -2778,13 +2785,10 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op [] let args = - if kind = Decimal then - [ str; makeIntConst style ] @ outValue - elif isFloatOrDecimal then - [ str ] @ outValue + if isFloatOrDecimal then + [ str; style ] @ outValue else - [ str; makeIntConst style; makeBoolConst unsigned; makeIntConst bitsize ] - @ outValue + [ str; style; makeBoolConst unsigned; makeIntConst bitsize ] @ outValue let callExpr = Helper.LibCall(com, numberModule, Naming.lowerFirst meth, t, args, ?loc = r) @@ -2793,6 +2797,36 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | Number(BigIntegers _, _) -> wrapLong com ctx r t callExpr | _ -> callExpr + let isFloatOrDecimalType = + match i.DeclaringEntityFullName with + | Types.decimal + | Types.float32 + | Types.float64 -> true + | _ -> false + + let supportedFloatStyles = + int ( + System.Globalization.NumberStyles.Any + &&& ~~~System.Globalization.NumberStyles.AllowCurrencySymbol + ) + + let checkFormatProvider meth hasStyle = + let acceptedArgs = + (if meth = "Parse" then + 1 + else + 2) + + (if hasStyle then + 1 + else + 0) + + match List.tryItem acceptedArgs args with + // InvariantCulture asks for exactly what Fable does, so there is nothing to report. + | None + | Some InvariantCulture -> () + | Some _ -> WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + let isFloat = match i.SignatureArgTypes with | Number(Floats _, _) :: _ -> true @@ -2825,55 +2859,34 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op Helper.LibCall(com, "Double", "isInfinity", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ("Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp"), _ -> operators com ctx r t i thisArg args - | ("Parse" | "TryParse") as meth, str :: NumberConst(NumberValue.Int32 style, _) :: _ -> - let hexConst = int System.Globalization.NumberStyles.HexNumber - let intConst = int System.Globalization.NumberStyles.Integer - - if - style <> hexConst - && style <> intConst - && i.DeclaringEntityFullName <> Types.decimal - then + | ("Parse" | "TryParse") as meth, str :: (ExprType(Number(Int32, _)) as style) :: _ -> + match style with + | NumberConst(NumberValue.Int32 style, _) when + (if isFloatOrDecimalType then + style &&& ~~~supportedFloatStyles <> 0 + else + style <> int System.Globalization.NumberStyles.HexNumber + && style <> int System.Globalization.NumberStyles.Integer) + -> WarningCodes.numberStylesIgnored style |> addWarningWithCode com ctx.InlinePath r + | _ -> () - let acceptedArgs = - if meth = "Parse" then - 2 - else - 3 - - match List.tryItem acceptedArgs args with - // InvariantCulture asks for exactly what Fable does, so there is nothing to report. - | None - | Some InvariantCulture -> () - | Some _ -> - // e.g. Double.Parse(string, style, IFormatProvider) etc. - WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r - + checkFormatProvider meth true parseCall meth str args style |> Some | ("Parse" | "TryParse") as meth, str :: _ -> - let acceptedArgs = - if meth = "Parse" then - 1 - else - 2 - - match List.tryItem acceptedArgs args with - // InvariantCulture asks for exactly what Fable does, so there is nothing to report. - | None - | Some InvariantCulture -> () - | Some _ -> - // e.g. Double.Parse(string, IFormatProvider) etc. - WarningCodes.formatProviderIgnored |> addWarningWithCode com ctx.InlinePath r + checkFormatProvider meth false let style = - if i.DeclaringEntityFullName = Types.decimal then - int System.Globalization.NumberStyles.Number - else - int System.Globalization.NumberStyles.Any - - parseCall meth str args style |> Some + match i.DeclaringEntityFullName with + | Types.decimal -> System.Globalization.NumberStyles.Number + | Types.float32 + | Types.float64 -> + System.Globalization.NumberStyles.Float + ||| System.Globalization.NumberStyles.AllowThousands + | _ -> System.Globalization.NumberStyles.Any + + parseCall meth str args (makeIntConst (int style)) |> Some | "Pow", _ -> Helper.GlobalCall("Math", t, args, i.SignatureArgTypes, memb = "pow", ?loc = r) |> Some diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index fa8f3a2c54..c9f2a42019 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -2,6 +2,7 @@ import Decimal, { BigSource } from "./lib/big.js"; import { Numeric, symbol } from "./Numeric.ts"; import { FSharpRef } from "./Types.ts"; import { Exception, combineHashCodes } from "./Util.ts"; +import { NumberStyles, splitNumber } from "./NumberStyles.ts"; import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; import { fromDecimal, int64, uint64, int128, uint128, nativeint, unativeint } from "./BigInt.ts"; @@ -133,64 +134,20 @@ export function toString(x: Decimal) { return x.toString(); } -const NumberStyles = { - AllowLeadingWhite: 1, - AllowTrailingWhite: 2, - AllowLeadingSign: 4, - AllowTrailingSign: 8, - AllowParentheses: 16, - AllowDecimalPoint: 32, - AllowThousands: 64, - AllowExponent: 128, - AllowHexSpecifier: 512, - Number: 111, -} as const; - -// The digits of a decimal string once whitespace, sign and parentheses are stripped -function digitsPattern(style: number): RegExp { - const integral = style & NumberStyles.AllowThousands ? "\\d+(?:,\\d+)*" : "\\d+"; - const point = style & NumberStyles.AllowDecimalPoint ? "(?:\\.\\d*)?" : ""; - const leadingPoint = style & NumberStyles.AllowDecimalPoint ? "|\\.\\d+" : ""; - const exponent = style & NumberStyles.AllowExponent ? "(?:[eE][+-]?\\d+)?" : ""; - return new RegExp(`^(?:${integral}${point}${leadingPoint})${exponent}$`); -} - // Returns undefined when the string does not have the format allowed by the style function parseWithStyle(str: string, style: number): Decimal | undefined { - if (style & NumberStyles.AllowHexSpecifier) { - throw new Exception("The number style AllowHexSpecifier is not supported on floating point data types."); - } - // .NET ignores trailing NULs and its white space is only [\t\n\v\f\r ] - let s = str.replace(/\0+$/, ""); - if (style & NumberStyles.AllowLeadingWhite) { - s = s.replace(/^[\t-\r ]+/, ""); - } - if (style & NumberStyles.AllowTrailingWhite) { - s = s.replace(/[\t-\r ]+$/, ""); - } - let negative = false; - if (style & NumberStyles.AllowParentheses && s.startsWith("(") && s.endsWith(")")) { - s = s.slice(1, -1); - negative = true; - } - if (style & NumberStyles.AllowLeadingSign && /^[+-]/.test(s)) { - negative = s[0] === "-" ? !negative : negative; - s = s.slice(1); - } else if (style & NumberStyles.AllowTrailingSign && /[+-]$/.test(s)) { - negative = s[s.length - 1] === "-" ? !negative : negative; - s = s.slice(0, -1); - } - if (!digitsPattern(style).test(s)) { + const parts = splitNumber(str, style); + if (parts === undefined) { return undefined; } let d: Decimal; try { - d = new Decimal(s.replace(/,/g, "")); + d = new Decimal(parts.digits); } catch { // Fable's big.js fork throws for values outside the decimal range throw OverflowException_$ctor_Z721C83C5(OVERFLOW); } - if (negative) { + if (parts.negative) { d.s = -1; } return d; diff --git a/src/fable-library-ts/Double.ts b/src/fable-library-ts/Double.ts index b8d42e6d6e..ccda405933 100644 --- a/src/fable-library-ts/Double.ts +++ b/src/fable-library-ts/Double.ts @@ -1,15 +1,18 @@ import { FSharpRef } from "./Types.ts"; import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; +import { NumberStyles, splitNumber } from "./NumberStyles.ts"; -// .NET's Float style: white space, a sign, digits with an optional fraction or a fraction alone, an exponent, then NULs -const floatRegex = /^[\t-\r ]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[\t-\r ]*\0*$/; // .NET's NaN and infinity symbols, ignoring case, allowing a sign and trimmed like String.Trim const symbolRegex = /^[\t-\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*(?:[+-]?nan|([+-]?)infinity)[\t-\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*$/i; -export function tryParse(str: string, defValue: FSharpRef): boolean { +const defaultStyle = NumberStyles.Float | NumberStyles.AllowThousands; + +export function tryParse(str: string, style: number, defValue: FSharpRef): boolean { if (str != null) { - if (floatRegex.test(str)) { - defValue.contents = Number.parseFloat(str); + const parts = splitNumber(str, style); + if (parts !== undefined) { + const value = Number.parseFloat(parts.digits); + defValue.contents = parts.negative ? -value : value; return true; } const symbol = symbolRegex.exec(str); @@ -21,9 +24,9 @@ export function tryParse(str: string, defValue: FSharpRef): boolean { return false; } -export function parse(str: string): number { +export function parse(str: string, style: number = defaultStyle): number { const defValue = new FSharpRef(0); - if (tryParse(str, defValue)) { + if (tryParse(str, style, defValue)) { return defValue.contents; } else if (str == null) { throw ArgumentNullException_$ctor_Z721C83C5("s"); @@ -33,7 +36,7 @@ export function parse(str: string): number { } export function parseIgnoringUnderscores(str: string): number { - return parse(str == null ? str : str.replace(/_/g, "")); + return parse(str == null ? str : str.replace(/_/g, ""), NumberStyles.Float); } // JS Number.isFinite function evals false for NaN diff --git a/src/fable-library-ts/NumberStyles.ts b/src/fable-library-ts/NumberStyles.ts new file mode 100644 index 0000000000..8e96aa5a8d --- /dev/null +++ b/src/fable-library-ts/NumberStyles.ts @@ -0,0 +1,63 @@ +import { Exception } from "./Util.ts"; + +export const NumberStyles = { + None: 0, + AllowLeadingWhite: 1, + AllowTrailingWhite: 2, + AllowLeadingSign: 4, + AllowTrailingSign: 8, + AllowParentheses: 16, + AllowDecimalPoint: 32, + AllowThousands: 64, + AllowExponent: 128, + AllowHexSpecifier: 512, + Integer: 7, + Number: 111, + Float: 167, +} as const; + +export interface NumberParts { + digits: string; + negative: boolean; +} + +// The digits of a number string once white space, sign and parentheses are stripped. +// .NET allows group separators anywhere after the first digit of the integral part, even repeated. +function digitsPattern(style: number): RegExp { + const integral = style & NumberStyles.AllowThousands ? "\\d+(?:,\\d*)*" : "\\d+"; + const point = style & NumberStyles.AllowDecimalPoint ? "(?:\\.\\d*)?" : ""; + const leadingPoint = style & NumberStyles.AllowDecimalPoint ? "|\\.\\d+" : ""; + const exponent = style & NumberStyles.AllowExponent ? "(?:[eE][+-]?\\d+)?" : ""; + return new RegExp(`^(?:${integral}${point}${leadingPoint})${exponent}$`); +} + +// Returns undefined when the string does not have the format allowed by the style +export function splitNumber(str: string, style: number): NumberParts | undefined { + if (style & NumberStyles.AllowHexSpecifier) { + throw new Exception("The number style AllowHexSpecifier is not supported on floating point data types."); + } + // .NET ignores trailing NULs and its white space is only [\t\n\v\f\r ] + let s = str.replace(/\0+$/, ""); + if (style & NumberStyles.AllowLeadingWhite) { + s = s.replace(/^[\t-\r ]+/, ""); + } + if (style & NumberStyles.AllowTrailingWhite) { + s = s.replace(/[\t-\r ]+$/, ""); + } + let negative = false; + if (style & NumberStyles.AllowParentheses && s.startsWith("(") && s.endsWith(")")) { + s = s.slice(1, -1); + negative = true; + } + if (style & NumberStyles.AllowLeadingSign && /^[+-]/.test(s)) { + negative = s[0] === "-" ? !negative : negative; + s = s.slice(1); + } else if (style & NumberStyles.AllowTrailingSign && /[+-]$/.test(s)) { + negative = s[s.length - 1] === "-" ? !negative : negative; + s = s.slice(0, -1); + } + if (!digitsPattern(style).test(s)) { + return undefined; + } + return { digits: s.replace(/,/g, ""), negative }; +} diff --git a/tests/Integration/Compiler/IgnoredArgumentTests.fs b/tests/Integration/Compiler/IgnoredArgumentTests.fs index a2af15784f..39b4b60a6b 100644 --- a/tests/Integration/Compiler/IgnoredArgumentTests.fs +++ b/tests/Integration/Compiler/IgnoredArgumentTests.fs @@ -127,6 +127,18 @@ Double.Parse("1.5", NumberStyles.Currency, CultureInfo.InvariantCulture) |> igno |> Assert.Code.warning "FABLE0103" |> ignore + testCase "A supported NumberStyles on a float is not reported" <| fun _ -> + let source = + """ +open System +open System.Globalization +Double.Parse("1.5", NumberStyles.Float ||| NumberStyles.AllowParentheses, CultureInfo.InvariantCulture) |> ignore +Decimal.Parse("1.5", NumberStyles.Number, CultureInfo.InvariantCulture) |> ignore +""" + compile source + |> Assert.Code.noWarning "FABLE0103" + |> ignore + testCase "A TimeSpan built with microseconds is reported" <| fun _ -> // The runtime representation only carries milliseconds, so the finer component is dropped. let source = diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 855dc8c9bf..9ef1fb71a3 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -28,11 +28,7 @@ let parse f (a: string) = // The one-argument overload depends on the current culture on .NET, and the explicit style would trigger FABLE0103 on Fable let tryParseFloat (s: string) = -#if FABLE_COMPILER - Double.TryParse s -#else Double.TryParse(s, NumberStyles.Float ||| NumberStyles.AllowThousands, CultureInfo.InvariantCulture) -#endif let private toPlainArray (bytes: byte[]) : byte[] = #if FABLE_COMPILER @@ -145,6 +141,47 @@ let tests = (fun () -> parse "0x10") |> throwsAnyError (fun () -> parse "1_0") |> throwsAnyError + testCase "System.Double.Parse accepts group separators like invariant .NET" <| fun () -> + let parse (s: string) = Double.Parse(s, CultureInfo.InvariantCulture) + parse "1,5" |> equal 15.0 + parse "1,000.5" |> equal 1000.5 + parse "1,,0" |> equal 10.0 + parse "1," |> equal 1.0 + parse "1,.5" |> equal 1.5 + parse "1,e2" |> equal 100.0 + parse "-1,000" |> equal -1000.0 + Single.Parse("1,5", CultureInfo.InvariantCulture) |> equal 15.0f + Convert.ToDouble("1,000", CultureInfo.InvariantCulture) |> equal 1000.0 + tryParseFloat "1,000" |> equal (true, 1000.0) + for s in [ ","; ",1"; "1.5,0"; "1e1,0"; "-,1" ] do + (fun () -> parse s) |> throwsAnyError + (fun () -> float "1,5") |> throwsAnyError + (fun () -> float32 "1,5") |> throwsAnyError + + testCase "System.Double.Parse honors NumberStyles" <| fun () -> + let parse (s: string) (style: NumberStyles) = Double.Parse(s, style, CultureInfo.InvariantCulture) + parse "1e3" NumberStyles.Float |> equal 1000.0 + parse "1,000" NumberStyles.Number |> equal 1000.0 + parse "1.5-" NumberStyles.Number |> equal -1.5 + parse "(1.5)" (NumberStyles.Float ||| NumberStyles.AllowParentheses) |> equal -1.5 + parse "1" NumberStyles.None |> equal 1.0 + for s, style in [ "1,5", NumberStyles.Float; "1e3", NumberStyles.Number; "1.5", NumberStyles.Integer; " 1 ", NumberStyles.None; "-1", NumberStyles.None; "+1", NumberStyles.AllowTrailingSign ] do + (fun () -> parse s style) |> throwsAnyError + let result = ref 0.0 + Double.TryParse("1,5", NumberStyles.Float, CultureInfo.InvariantCulture, result) |> equal false + Double.TryParse("1,5", NumberStyles.Number, CultureInfo.InvariantCulture, result) |> equal true + result.Value |> equal 15.0 + Single.Parse("1,5", NumberStyles.Float ||| NumberStyles.AllowThousands, CultureInfo.InvariantCulture) |> equal 15.0f + (fun () -> Single.Parse("1,5", NumberStyles.Float, CultureInfo.InvariantCulture)) |> throwsAnyError + + testCase "System.Decimal.Parse accepts .NET's group separator placements" <| fun () -> + let parse (s: string) = Decimal.Parse(s, CultureInfo.InvariantCulture) + parse "1,,0" |> equal 10M + parse "1," |> equal 1M + parse "1,.5" |> equal 1.5M + for s in [ ","; ",1"; "1.5,0" ] do + (fun () -> parse s) |> throwsAnyError + testCase "Double.Parse and Convert.ToDouble reject underscores" <| fun () -> for s in [ "1_0"; "_1"; "1_"; "_"; "1e1_0"; "1._5" ] do (fun () -> Double.Parse(s, CultureInfo.InvariantCulture)) |> throwsAnyError From e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711 Mon Sep 17 00:00:00 2001 From: Goswin Rothenthal Date: Wed, 28 Oct 2026 12:15:14 +0100 Subject: [PATCH 267/322] fix(ts): annotate Fable.Core typed arrays with native TS types Fable.Core aliases like `Float32Array = TypedArray` are erased by FCS, so values of these types were annotated as `TypedArray`, which does not exist in TypeScript. Map `TypedArray` to the native typed array type. Co-Authored-By: Claude Opus 5.5 --- src/Fable.Transforms/Babel/Fable2Babel.fs | 19 +++++++++++++++++ tests/TypeScript/LiteTsInteropTests.fs | 25 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 89cb0d41e1..efd20ab145 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -869,6 +869,25 @@ module Annotation = | Types.ievent2 -> makeFableLibImportTypeAnnotation com ctx genArgs "Event" "IEvent$2" |> Some | "Fable.Core.JS.Set`1" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "ISet" |> Some | "Fable.Core.JS.Map`2" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "IMap" |> Some + // FCS erases the Fable.Core aliases, so only TypedArray reaches here + | "Fable.Core.JS.TypedArray`1" -> + match genArgs with + | [ Fable.Number(kind, _) ] -> + match kind with + | Int8 -> Some "Int8Array" + | UInt8 -> Some "Uint8Array" + | Int16 -> Some "Int16Array" + | UInt16 -> Some "Uint16Array" + | Int32 -> Some "Int32Array" + | UInt32 -> Some "Uint32Array" + | Int64 + | BigInt -> Some "BigInt64Array" + | UInt64 -> Some "BigUint64Array" + | Float32 -> Some "Float32Array" + | Float64 -> Some "Float64Array" + | _ -> None + | _ -> None + |> Option.map (makeNativeTypeAnnotation com ctx []) | _ -> None let makeStringEnumTypeAnnotation (ent: Fable.Entity) (attArgs: obj list) = diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index fe45121f4f..f033878bb7 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -41,6 +41,19 @@ module TaggedUnion = | Take of Take | Edit of edit: Edit | Save + +// Top level values get TS annotations, so tsc checks the typed array type names exist +module TypedArrays = + let int8s = JS.Constructors.Int8Array.Create [| 1y; -2y |] + let uint8s = JS.Constructors.Uint8Array.Create [| 1uy; 2uy |] + let int16s = JS.Constructors.Int16Array.Create [| 1s; -2s |] + let uint16s = JS.Constructors.Uint16Array.Create [| 1us; 2us |] + let int32s = JS.Constructors.Int32Array.Create [| 1; -2 |] + let uint32s = JS.Constructors.Uint32Array.Create [| 1u; 2u |] + let float32s = JS.Constructors.Float32Array.Create [| 1.5; -2.5 |] + let float64s = JS.Constructors.Float64Array.Create [| 1.5; -2.5 |] + let bigInt64s = JS.Constructors.BigInt64Array.Create 2 + let makeFloat32s () = JS.Constructors.Float32Array.Create [| 0.0; 1.0 |] #endif let tests = @@ -92,5 +105,17 @@ let tests = TaggedUnion.Command.Edit { new TaggedUnion.Edit with member _.text = "hi" } |> describe |> equal "edit: hi" TaggedUnion.Command.Save |> describe |> equal "save" !!{| ``type`` = "take"; fromIndex = 2; toIndex = 5 |} |> describe |> equal "take: 2-5" + + testCase "Fable.Core typed arrays are annotated with native TS types" <| fun () -> + TypedArrays.int8s.[1] |> equal -2y + TypedArrays.uint8s.[1] |> equal 2uy + TypedArrays.int16s.[1] |> equal -2s + TypedArrays.uint16s.[1] |> equal 2us + TypedArrays.int32s.[1] |> equal -2 + TypedArrays.uint32s.[1] |> equal 2u + TypedArrays.float32s.[1] |> equal -2.5f + TypedArrays.float64s.[1] |> equal -2.5 + TypedArrays.bigInt64s.length |> equal 2 + TypedArrays.makeFloat32s().[1] |> equal 1.0f #endif ] From f790bf534fa6a242c1a302c76512e07054f4feb2 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Wed, 28 Oct 2026 22:29:17 +0100 Subject: [PATCH 268/322] fix(ts): give Uint8ClampedArray its own Fable.Core type --- src/Fable.Core/Fable.Core.JS.fs | 3 ++- src/Fable.Transforms/Babel/Fable2Babel.fs | 1 + tests/TypeScript/LiteTsInteropTests.fs | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Fable.Core/Fable.Core.JS.fs b/src/Fable.Core/Fable.Core.JS.fs index 880df39172..aad46ff6e0 100644 --- a/src/Fable.Core/Fable.Core.JS.fs +++ b/src/Fable.Core/Fable.Core.JS.fs @@ -587,7 +587,8 @@ module JS = abstract Create: data: obj -> Uint8Array - and Uint8ClampedArray = TypedArray + and Uint8ClampedArray = + inherit TypedArray and Uint8ClampedArrayConstructor = [] diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index efd20ab145..cda3307bce 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -869,6 +869,7 @@ module Annotation = | Types.ievent2 -> makeFableLibImportTypeAnnotation com ctx genArgs "Event" "IEvent$2" |> Some | "Fable.Core.JS.Set`1" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "ISet" |> Some | "Fable.Core.JS.Map`2" -> makeFableLibImportTypeAnnotation com ctx genArgs "Util" "IMap" |> Some + | "Fable.Core.JS.Uint8ClampedArray" -> makeNativeTypeAnnotation com ctx [] "Uint8ClampedArray" |> Some // FCS erases the Fable.Core aliases, so only TypedArray reaches here | "Fable.Core.JS.TypedArray`1" -> match genArgs with diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index f033878bb7..5e70897578 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -46,6 +46,7 @@ module TaggedUnion = module TypedArrays = let int8s = JS.Constructors.Int8Array.Create [| 1y; -2y |] let uint8s = JS.Constructors.Uint8Array.Create [| 1uy; 2uy |] + let uint8Clamped = JS.Constructors.Uint8ClampedArray.Create [| 1; 300 |] let int16s = JS.Constructors.Int16Array.Create [| 1s; -2s |] let uint16s = JS.Constructors.Uint16Array.Create [| 1us; 2us |] let int32s = JS.Constructors.Int32Array.Create [| 1; -2 |] @@ -109,6 +110,7 @@ let tests = testCase "Fable.Core typed arrays are annotated with native TS types" <| fun () -> TypedArrays.int8s.[1] |> equal -2y TypedArrays.uint8s.[1] |> equal 2uy + TypedArrays.uint8Clamped.[1] |> equal 255uy TypedArrays.int16s.[1] |> equal -2s TypedArrays.uint16s.[1] |> equal 2us TypedArrays.int32s.[1] |> equal -2 From ce886964cb4521dc4c01c8da089b97c9d9742814 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 29 Oct 2026 14:57:11 +0100 Subject: [PATCH 269/322] fix(all): keep F# diagnostics for scripts on Windows (#5218) --- src/Fable.Cli/Main.fs | 6 +++- src/Fable.Compiler/Library.fs | 61 ++++++++++++++++++++++++---------- src/Fable.Compiler/Library.fsi | 6 ++++ 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/Fable.Cli/Main.fs b/src/Fable.Cli/Main.fs index 173eb7c806..624f8771e5 100644 --- a/src/Fable.Cli/Main.fs +++ b/src/Fable.Cli/Main.fs @@ -596,11 +596,13 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl (fun () -> let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader sourceFiles + let filePaths, sourceReader = CodeServices.checkerFileNames filePaths sourceReader + checker.ParseAndCheckProject( projCracked.ProjectFile, filePaths, sourceReader, - lastFile, + CodeServices.checkerFileName lastFile, ?subscriber = subscriber ) ) @@ -951,6 +953,8 @@ and FableCompiler(checker: InteractiveChecker, projCracked: ProjectCracked, fabl let filePaths, sourceReader = Fable.Compiler.File.MakeSourceReader projCracked.SourceFiles + let filePaths, sourceReader = CodeServices.checkerFileNames filePaths sourceReader + checker.Compile(filePaths, sourceReader, outFile) type Watcher = diff --git a/src/Fable.Compiler/Library.fs b/src/Fable.Compiler/Library.fs index 7d850edfa0..8f6e87daf6 100644 --- a/src/Fable.Compiler/Library.fs +++ b/src/Fable.Compiler/Library.fs @@ -106,6 +106,21 @@ type BabelWriter module CodeServices = + // FCS keeps the first spelling registered for a file and drops diagnostics whose file name differs from it + let checkerFileName (fileName: string) = + (FSharp.Compiler.Text.Range.mkFirstLineOfFile fileName).FileName + + let checkerFileNames (fileNames: string[]) (sourceReader: SourceReader) = + let checkerNames = Array.map checkerFileName fileNames + let originalNames = Array.zip checkerNames fileNames |> Map.ofArray + + let checkerSourceReader fileName = + match Map.tryFind fileName originalNames with + | Some originalName -> sourceReader originalName + | None -> sourceReader fileName + + checkerNames, checkerSourceReader + let getFSharpDiagnostics (diagnostics: FSharpDiagnostic array) = diagnostics |> Array.map (fun er -> @@ -132,7 +147,7 @@ module CodeServices = let msg = $"%s{er.Message} (code %i{er.ErrorNumber})" - LogEntry.Make(severity, msg, fileName = er.FileName, range = range, tag = "FSHARP") + LogEntry.Make(severity, msg, fileName = Path.normalizePath er.FileName, range = range, tag = "FSHARP") ) let compileFileToJs @@ -166,12 +181,11 @@ module CodeServices = async { let! assemblies = checker.GetImportedAssemblies() + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + let! checkProjectResult = - checker.ParseAndCheckProject( - cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader - ) + checker.ParseAndCheckProject(cliArgs.ProjectFile, fileNames, checkerSourceReader) return { @@ -191,13 +205,16 @@ module CodeServices = async { let! assemblies = checker.GetImportedAssemblies() + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + // Type-check the project up until the current file. let! checkProjectResult = checker.ParseAndCheckProject( cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader, - lastFile = currentFile + fileNames, + checkerSourceReader, + lastFile = checkerFileName currentFile ) let fableProj = @@ -326,12 +343,22 @@ module CodeServices = crackerResponse.ProjectOptions.SourceFiles |> Array.tryFind (fun f -> f = String.Concat(currentFile, "i")) + let fileNames, checkerSourceReader = + checkerFileNames crackerResponse.ProjectOptions.SourceFiles sourceReader + + let originalFileNames = + Array.zip fileNames crackerResponse.ProjectOptions.SourceFiles |> Map.ofArray + let! dependentFiles = - match signatureFile with - | None -> - checker.GetDependentFiles(currentFile, crackerResponse.ProjectOptions.SourceFiles, sourceReader) - | Some signatureFile -> - checker.GetDependentFiles(signatureFile, crackerResponse.ProjectOptions.SourceFiles, sourceReader) + checker.GetDependentFiles( + checkerFileName (Option.defaultValue currentFile signatureFile), + fileNames, + checkerSourceReader + ) + + let dependentFiles = + dependentFiles + |> Array.map (fun f -> Map.tryFind f originalFileNames |> Option.defaultValue f) Log.info ( sprintf @@ -352,9 +379,9 @@ module CodeServices = let! checkProjectResult = checker.ParseAndCheckProject( cliArgs.ProjectFile, - crackerResponse.ProjectOptions.SourceFiles, - sourceReader, - lastFile = lastFile + fileNames, + checkerSourceReader, + lastFile = checkerFileName lastFile ) let fableProj = diff --git a/src/Fable.Compiler/Library.fsi b/src/Fable.Compiler/Library.fsi index fa54ce00ad..025434d8aa 100644 --- a/src/Fable.Compiler/Library.fsi +++ b/src/Fable.Compiler/Library.fsi @@ -37,6 +37,12 @@ type TypeCheckProjectResult = [] module CodeServices = + /// The spelling the F# checker uses for a file, which is the one its diagnostics carry. + val checkerFileName: fileName: string -> string + + /// The file names as the F# checker spells them, with a source reader that still receives the names given here. + val checkerFileNames: fileNames: string[] -> sourceReader: SourceReader -> string[] * SourceReader + /// Convert the diagnostics of the F# compiler into log entries tagged "FSHARP". /// The error number is folded into the message, as `Fable.Cli` prints it. val getFSharpDiagnostics: diagnostics: FSharpDiagnostic array -> LogEntry array From 17b531a603c06a000dce9a3f065e77aa000d5556 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:12:21 +0100 Subject: [PATCH 270/322] chore: release multiple projects (#5170) --- src/Fable.Cli/CHANGELOG.md | 53 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 52 +++++++++++++++++++++++- src/Fable.Core/CHANGELOG.md | 10 ++++- src/Fable.Transforms/Global/Compiler.fs | 4 +- src/fable-compiler-js/CHANGELOG.md | 40 ++++++++++++++++++- src/fable-compiler-js/package.json | 4 +- src/fable-library-ts/CHANGELOG.md | 34 +++++++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 51 +++++++++++++++++++++++- src/fable-standalone/package.json | 4 +- 10 files changed, 241 insertions(+), 13 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index e6abb87713..9d6215b9a4 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,57 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.25.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(all)* Keep F# diagnostics for scripts on Windows (#5218) ([ce886964](https://github.com/fable-compiler/Fable/commit/ce886964cb4521dc4c01c8da089b97c9d9742814)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Keep column 0 mappings in source maps (#5176) ([04ba3853](https://github.com/fable-compiler/Fable/commit/04ba3853620811f6ed66eb6812bd9df3d87a1b5d)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 5.24.0 - 2026-10-22 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 3ead4b55d5..7fcc218130 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,56 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.24.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(all)* Keep F# diagnostics for scripts on Windows (#5218) ([ce886964](https://github.com/fable-compiler/Fable/commit/ce886964cb4521dc4c01c8da089b97c9d9742814)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 5.23.0 - 2026-10-22 ### 🚀 Features diff --git a/src/Fable.Core/CHANGELOG.md b/src/Fable.Core/CHANGELOG.md index 96f35da731..ad0ca135ff 100644 --- a/src/Fable.Core/CHANGELOG.md +++ b/src/Fable.Core/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 --- # Changelog @@ -9,6 +9,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.6.1 - 2026-10-29 + +### 🐞 Bug Fixes + +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 5.6.0 - 2026-10-22 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index b4e6f72d09..128caf6123 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.24.0" + let VERSION = "5.25.0" [] - let JS_LIBRARY_VERSION = "2.10.0" + let JS_LIBRARY_VERSION = "2.11.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index f0f881060f..46e9686e9c 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.3.0 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 4.2.0 - 2026-10-22 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 9f09e22bbf..698c05f08d 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "4.2.0", + "version": "4.3.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.2.0", - "@fable-org/fable-standalone": "^4.0.0-beta.3" + "@fable-org/fable-standalone": "^4.0.0-beta.4" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 81142079de..00c444f24a 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 updaters: - package.json: file: package.json @@ -15,6 +15,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.11.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 2.10.0 - 2026-10-22 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index 8bcda262e0..1685a2a430 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.10.0", + "version": "2.11.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 4f936ea804..7760b1ebd2 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: 2098f64a25e96385306ef19ed8c289f3b6c84db9 +last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 pre_release: beta include: - ../Fable.Transforms/ @@ -21,6 +21,55 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.4 - 2026-10-29 + +### 🚀 Features + +* *(all)* Error on unsupported RegexOptions at compile time (#5180) ([a8cbc5dc](https://github.com/fable-compiler/Fable/commit/a8cbc5dc0c3a96639028057e132b39e559f40ced)) +* *(js/ts/python/dart)* Support Nullable conversions (#5183) ([6efa86d3](https://github.com/fable-compiler/Fable/commit/6efa86d35c5943dc8bbb6da0f27cc37095276064)) +* *(js/ts/python/dart/beam)* Support System.Array.BinarySearch and ResizeArray.BinarySearch (#5172) ([d332e7d4](https://github.com/fable-compiler/Fable/commit/d332e7d42c889f172bb9501f505292abf519752f)) + +### 🐞 Bug Fixes + +* *(all)* Warn on duplicate names in erased unions (#5169) ([459166d2](https://github.com/fable-compiler/Fable/commit/459166d2500a963a3616d8b1a63e1d8585d61ec6)) +* *(all)* Recompile inline users of a file with a signature (#5179) ([b383673d](https://github.com/fable-compiler/Fable/commit/b383673da61cfe3ca06416a624aaafbd732f876c)) +* *(all)* Accept RegexOptions.NonBacktracking (#5178) ([a61ec8a1](https://github.com/fable-compiler/Fable/commit/a61ec8a1583830599a7ffb2dfb4d093e99902d4f)) +* *(beam)* Raise ArgumentNullException from nullArg ([0b8f77f9](https://github.com/fable-compiler/Fable/commit/0b8f77f9880bcbd9317272d9b0d89f8fe8ac0257)) +* *(beam)* Keep parameter name in ArgumentException ([9e5d34f7](https://github.com/fable-compiler/Fable/commit/9e5d34f7b32cfdfb8b1adc93378895b26a5d69b9)) +* *(js/ts)* Format DateTimeOffset in its own offset (#5171) ([04ddbd20](https://github.com/fable-compiler/Fable/commit/04ddbd2027e03f4cf7b0b22fe886fdabadf26d93)) +* *(js/ts)* Support %o in sprintf and interpolated strings (#5175) ([e13e7b0f](https://github.com/fable-compiler/Fable/commit/e13e7b0fdc16d26793833c77a11d242ccc61109b)) +* *(js/ts)* Zero-fill Array.zeroCreate for numeric types without a typed array (#5211) ([d96b6ab8](https://github.com/fable-compiler/Fable/commit/d96b6ab82f0181be4a379c806abcedde23f1359a)) +* *(js/ts)* Throw on out-of-range string indexer (#5214) ([d5f1f8fd](https://github.com/fable-compiler/Fable/commit/d5f1f8fdfab535eaff5c492661d0178be1c4c76a)) +* *(js/ts)* Parse numbers like .NET and raise its exception types (#5200) ([82e606e9](https://github.com/fable-compiler/Fable/commit/82e606e9f8567d9bdad743df1b5de6abbdda89b0)) +* *(js/ts)* Honor NumberStyles when parsing floats (#5217) ([82b935f3](https://github.com/fable-compiler/Fable/commit/82b935f3859b36923e5e0eb26771f860eadbe525)) +* *(js/ts/dart)* Float to small integer and char conversions (#5182) ([bb0faa79](https://github.com/fable-compiler/Fable/commit/bb0faa794080a04e7791d419b2fa4d2ad107c186)) +* *(js/ts/python)* Compare ReferenceEquality types by ref (#5174) ([9c351626](https://github.com/fable-compiler/Fable/commit/9c3516268674b54aec2efae381cad920d4f15922)) +* *(js/ts/python/dart/beam)* Raise typed exceptions from invalidArg and invalidOp ([60f2f5b9](https://github.com/fable-compiler/Fable/commit/60f2f5b9fa0e6a26af0f3b4f3cddc2496f075ba3)) +* *(js/ts/python/dart/beam)* Align collection exception types with FSharp.Core (#5153) ([7d7ceb33](https://github.com/fable-compiler/Fable/commit/7d7ceb33afd85a25bf06dc703e9d4a471cbc3292)) +* *(js/ts/python/dart/beam)* Add FSharp.Core's argument checks to collection functions (#5154) ([5185e654](https://github.com/fable-compiler/Fable/commit/5185e6547416384fe85fcb0b0c1cf267708879db)) +* *(python/beam/rust)* Implicit DateTime to DateTimeOffset (#5173) ([401c881f](https://github.com/fable-compiler/Fable/commit/401c881f729494d06d20da9687e78f7315f5efb2)) +* *(ts)* Annotate Fable.Core typed arrays with native TS types ([e35d06c3](https://github.com/fable-compiler/Fable/commit/e35d06c36ba8ce4ab6cce75d6b332b9dcd50d711)) +* *(ts)* Give Uint8ClampedArray its own Fable.Core type ([f790bf53](https://github.com/fable-compiler/Fable/commit/f790bf534fa6a242c1a302c76512e07054f4feb2)) + +### ⚡ Performance Improvements + +* *(js/ts)* Standard library combine numeric hash from snapshot of object instead of mapping (#5151) ([44495104](https://github.com/fable-compiler/Fable/commit/44495104b5c7ac65bfcea184d1305fd29f1db9b4)) +* *(js/ts)* Standard library remove .map result array from record hash using snapshot (#5150) ([0ceb5e9e](https://github.com/fable-compiler/Fable/commit/0ceb5e9ec5b470d00295a5cea9ead1c621c7858e)) +* *(js/ts)* Standard library one pass List.append builder (#5148) ([2ed9d6dd](https://github.com/fable-compiler/Fable/commit/2ed9d6dd0a372e5bcf53dfe24d786bf9b9256fd3)) +* *(js/ts)* Cast erased option unwraps in TypeScript output (#5181) ([65cd419d](https://github.com/fable-compiler/Fable/commit/65cd419d78e4dbc2008f8a707d5298e390defbee)) +* *(js/ts)* List node as a class with a shared empty node (#5185) ([187f8985](https://github.com/fable-compiler/Fable/commit/187f89852413186b52d4ecdde55e1be7cf736785)) +* *(js/ts)* Compile isNull to a plain null check (#5186) ([71a0ad92](https://github.com/fable-compiler/Fable/commit/71a0ad92237586e9569fd1483f35c88f8477f478)) +* *(js/ts)* Check array bounds only on undefined reads (#5188) ([435fc0bb](https://github.com/fable-compiler/Fable/commit/435fc0bb008ff2ab7c14a08c7218be94f4735029)) +* *(js/ts)* Closure-free ref cells and out-arg value cells (#5189) ([91bca925](https://github.com/fable-compiler/Fable/commit/91bca925757356191c7e6e7a71cb3e8a1bc8d52d)) +* *(js/ts)* Keep the curried original on its adapter (#5191) ([bfb4a2a4](https://github.com/fable-compiler/Fable/commit/bfb4a2a4e3bb1b5bd4491955a1e1cb90c14050b6)) +* *(js/ts)* Walk arrays by index when enumerated as seq (#5192) ([d88621c3](https://github.com/fable-compiler/Fable/commit/d88621c3b9d6ce48a72dc09b86a6805f612ac185)) +* *(js/ts)* Throw invalidArg and invalidOp at the call site (#5194) ([e6a742b1](https://github.com/fable-compiler/Fable/commit/e6a742b1ebda71342d4445705ea9198c2059ae90)) +* *(js/ts)* Hash unions without allocating (#5195) ([2b66a5d2](https://github.com/fable-compiler/Fable/commit/2b66a5d2184f3a35f0d73fb397ab902e300ac4bd)) +* *(js/ts)* Probe dictionary buckets without allocating (#5196) ([9517eafd](https://github.com/fable-compiler/Fable/commit/9517eafd93aa7b84061662a421b7a39a7269318b)) +* *(js/ts)* Pass array literals to typed array constructors as plain arrays (#5209) ([cb7d6d6e](https://github.com/fable-compiler/Fable/commit/cb7d6d6e49bb43b83afe80477ea2d6d667be3f5b)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/2098f64a25e96385306ef19ed8c289f3b6c84db9..ce886964cb4521dc4c01c8da089b97c9d9742814) + ## 4.0.0-beta.3 - 2026-10-22 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 93dc09c189..36582fb96f 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "4.0.0-beta.3", - "fableVersion": "5.24.0", + "version": "4.0.0-beta.4", + "fableVersion": "5.25.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 8ed7778893bab48c7dd41d7322d26d2bcaba6d99 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Thu, 29 Oct 2026 12:03:53 -0700 Subject: [PATCH 271/322] feat(js/ts/python/dart/beam): support ArgumentException.ParamName (#5203) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Beam/Replacements.fs | 88 +++++++++++++++---- src/Fable.Transforms/Dart/Replacements.fs | 1 + src/Fable.Transforms/Python/Replacements.fs | 1 + src/Fable.Transforms/Replacements.fs | 1 + src/fable-library-beam/fable_int.erl | 6 +- src/fable-library-beam/fable_list.erl | 9 +- src/fable-library-beam/fable_resize_array.erl | 3 +- src/fable-library-py/fable_library/System.fs | 18 ++-- src/fable-library-ts/System.fs | 18 ++-- tests/Beam/ExceptionTests.fs | 43 +++++++++ tests/Dart/src/MiscTests.fs | 27 ++++++ tests/Js/Main/MiscTests.fs | 27 ++++++ tests/Python/TestMisc.fs | 28 ++++++ 13 files changed, 233 insertions(+), 37 deletions(-) diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index c0c94f7b81..11b28ecf45 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -334,19 +334,49 @@ let private operators makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => invalid_operation_exception, message => $0}") |> Some | "InvalidArg", [ argName; msg ] -> - let msg = - add msg (add (add (Value(StringConstant " (Parameter '", None)) argName) (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_exception, message => $0}") + captureExprOnce + com + "param_name" + argName + (fun argName -> + let msg = + add + msg + (add + (add (Value(StringConstant " (Parameter '", None)) argName) + (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; argName ] + "#{exn_type => argument_exception, message => $0, param_name => $1}") + ) |> Some | "Raise", [ arg ] -> makeThrow r _t arg |> Some | "NullArg", [ arg ] -> - let msg = - add - (Value(StringConstant "Value cannot be null. (Parameter '", None)) - (add arg (Value(StringConstant "')", None))) - - makeThrow r _t (emitExpr r Any [ msg ] "#{exn_type => argument_null_exception, message => $0}") + captureExprOnce + com + "param_name" + arg + (fun arg -> + let msg = + add + (Value(StringConstant "Value cannot be null. (Parameter '", None)) + (add arg (Value(StringConstant "')", None))) + + makeThrow + r + _t + (emitExpr + r + Any + [ msg; arg ] + "#{exn_type => argument_null_exception, message => $0, param_name => $1}") + ) |> Some | "IsNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =:= undefined)" |> Some | "IsNotNull", [ arg ] -> emitExpr r _t [ arg ] "($0 =/= undefined)" |> Some @@ -6274,9 +6304,6 @@ let tryCall | Some inner -> emitExpr r t [ msg; inner ] $"#{{exn_type => %s{typeTag}, message => $0, inner_exception => $1}}" - let withParamName msg paramName = - Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) - let isArgumentException = typeName = "System.ArgumentException" // ArgumentNullException and ArgumentOutOfRangeException take (paramName, message), the others (message, paramName) @@ -6284,6 +6311,31 @@ let tryCall typeName = "System.ArgumentNullException" || typeName = "System.ArgumentOutOfRangeException" + // message and paramName are evaluated once, in source order + let makeArg msg paramName inner = + let build msg paramName = + let msg = + Helper.LibCall(com, "fable_utils", "argument_exception_message", String, [ msg; paramName ]) + + match inner with + | None -> + emitExpr r t [ msg; paramName ] $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1}}" + | Some inner -> + emitExpr + r + t + [ msg; paramName; inner ] + $"#{{exn_type => %s{typeTag}, message => $0, param_name => $1, inner_exception => $2}}" + + if paramNameFirst then + captureExprs2Once + com + ("param_name", paramName) + ("message", msg) + (fun paramName msg -> build msg paramName) + else + captureExprs2Once com ("message", msg) ("param_name", paramName) build + let defaultMessage = match typeName with | "System.ArgumentException" -> "Value does not fall within the expected range." @@ -6294,19 +6346,21 @@ let tryCall match info.CompiledName, thisArg, args with | ".ctor", None, [] -> make (makeStrConst defaultMessage) None |> Some | ".ctor", None, [ paramName ] when paramNameFirst -> - make (withParamName (makeStrConst defaultMessage) paramName) None |> Some + makeArg (makeStrConst defaultMessage) paramName None |> Some | ".ctor", None, [ msg ] -> make msg None |> Some | ".ctor", None, [ first; second ] -> match info.SignatureArgTypes with - | [ _; String ] when paramNameFirst -> make (withParamName second first) None |> Some - | [ _; String ] when isArgumentException -> make (withParamName first second) None |> Some + | [ _; String ] when paramNameFirst -> makeArg second first None |> Some + | [ _; String ] when isArgumentException -> makeArg first second None |> Some | [ _; String ] -> make first None |> Some | _ -> make first (Some second) |> Some | ".ctor", None, [ msg; paramName; inner ] when isArgumentException -> - make (withParamName msg paramName) (Some inner) |> Some + makeArg msg paramName (Some inner) |> Some + | ".ctor", None, [ paramName; _; msg ] when paramNameFirst -> makeArg msg paramName None |> Some | ".ctor", None, [ msg; _; inner ] -> make msg (Some inner) |> Some | "get_Message", Some c, _ -> emitExpr r t [ c ] "maps:get(message, $0, $0)" |> Some | "get_InnerException", Some c, _ -> emitExpr r t [ c ] "maps:get(inner_exception, $0, undefined)" |> Some + | "get_ParamName", Some c, _ -> emitExpr r t [ c ] "maps:get(param_name, $0, undefined)" |> Some | _ -> None // System.Type (reflection) — type info is a map #{fullname => ..., generics => [...]} | "System.Type" -> diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 354372048a..ce6705fd06 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2721,6 +2721,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> Helper.InstanceCall(e, "toString", t, [], ?loc = r) |> Some // | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 3da6ef0dfd..0f9ae88991 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2922,6 +2922,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr // Caught exceptions are typed as Python's built-in Exception, which lacks this attribute. Helper.GlobalCall("getattr", t, [ e; makeStrConst "inner_exception" ], ?loc = r) |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index bb1331fb10..9a930256d5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3334,6 +3334,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "get_Message", Some e -> getFieldWith r t e "message" |> Some | "get_StackTrace", Some e -> getFieldWith r t e "stack" |> Some | "get_InnerException", Some e -> getFieldWith r t e "innerException" |> Some + | "get_ParamName", Some _ -> bclType com ctx r t i thisArg args | _ -> None let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = diff --git a/src/fable-library-beam/fable_int.erl b/src/fable-library-beam/fable_int.erl index 9113fa354b..d75ed94194 100644 --- a/src/fable-library-beam/fable_int.erl +++ b/src/fable-library-beam/fable_int.erl @@ -87,7 +87,8 @@ wrap_u64(N) -> log2(N) when N < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"Non-negative number required. (Parameter 'value')">> + message => <<"Non-negative number required. (Parameter 'value')">>, + param_name => <<"value">> }); log2(0) -> 0; @@ -140,7 +141,8 @@ bigint_from_byte_array(BytesValue) -> bigint_pow(_Base, Exponent) when Exponent < 0 -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">> + message => <<"The number must be greater than or equal to zero. (Parameter 'exponent')">>, + param_name => <<"exponent">> }); bigint_pow(Base, Exponent) -> bigint_pow(Base, Exponent, 1). diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index ea7d918ecb..1cc2b2c97e 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -909,19 +909,22 @@ randomizer_out_of_range(R) -> "The index is outside the legal range.~nrandomizer returned ~w, should be in range [0.0, 1.0). (Parameter 'randomizer')", [R] ) - ) + ), + param_name => <<"randomizer">> }). invalid_arg(ParamName, Message) -> erlang:error(#{ exn_type => argument_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). argument_out_of_range(ParamName, Message) -> erlang:error(#{ exn_type => argument_out_of_range_exception, - message => fable_utils:argument_exception_message(Message, ParamName) + message => fable_utils:argument_exception_message(Message, ParamName), + param_name => ParamName }). lists_had_different_lengths(ParamName) -> diff --git a/src/fable-library-beam/fable_resize_array.erl b/src/fable-library-beam/fable_resize_array.erl index c24a6df568..ec6335786d 100644 --- a/src/fable-library-beam/fable_resize_array.erl +++ b/src/fable-library-beam/fable_resize_array.erl @@ -168,7 +168,8 @@ non_negative_number_required(ParamName) -> erlang:error(#{ exn_type => argument_out_of_range_exception, message => - fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName) + fable_utils:argument_exception_message(<<"Non-negative number required.">>, ParamName), + param_name => ParamName }). %% Find first matching predicate, return Default when not found diff --git a/src/fable-library-py/fable_library/System.fs b/src/fable-library-py/fable_library/System.fs index a85c921c46..ca77ee92ae 100644 --- a/src/fable-library-py/fable_library/System.fs +++ b/src/fable-library-py/fable_library/System.fs @@ -76,21 +76,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/src/fable-library-ts/System.fs b/src/fable-library-ts/System.fs index 03cb86a83d..86182aa495 100644 --- a/src/fable-library-ts/System.fs +++ b/src/fable-library-ts/System.fs @@ -74,21 +74,25 @@ type ArgumentException(message: string, paramName: string, innerException: exn | innerException ) - // Use Unchecked.defaultof rather than a `null` literal for the absent inner - // exception: Fable erases nullable reference annotations, so a `null` literal + // Use Unchecked.defaultof rather than a `null` literal for the absent parameter name + // and inner exception: Fable erases nullable reference annotations, so a `null` literal // would be emitted against a non-nullable parameter type and fail type checking. - new() = ArgumentException(SR.Arg_ArgumentException, "", Unchecked.defaultof) - new(message) = ArgumentException(message, "", Unchecked.defaultof) + new() = ArgumentException(SR.Arg_ArgumentException, Unchecked.defaultof, Unchecked.defaultof) + new(message) = ArgumentException(message, Unchecked.defaultof, Unchecked.defaultof) new(message: string, paramName: string) = ArgumentException(message, paramName, Unchecked.defaultof) - new(message: string, innerException: exn | null) = ArgumentException(message, "", innerException) + + new(message: string, innerException: exn | null) + = + ArgumentException(message, Unchecked.defaultof, innerException) + member _.ParamName = paramName type ArgumentNullException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentNullException(paramName, SR.ArgumentNull_Generic) - new() = ArgumentNullException("") + new() = ArgumentNullException(Unchecked.defaultof) type ArgumentOutOfRangeException(paramName: string, message: string) = inherit ArgumentException(message, paramName, Unchecked.defaultof) new(paramName) = ArgumentOutOfRangeException(paramName, SR.Arg_ArgumentOutOfRangeException) - new() = ArgumentOutOfRangeException("") + new() = ArgumentOutOfRangeException(Unchecked.defaultof) diff --git a/tests/Beam/ExceptionTests.fs b/tests/Beam/ExceptionTests.fs index d6e9119c2b..b9bf615daa 100644 --- a/tests/Beam/ExceptionTests.fs +++ b/tests/Beam/ExceptionTests.fs @@ -200,3 +200,46 @@ let ``test nullArg raises ArgumentNullException`` () = | :? System.ArgumentNullException as e -> e.Message | e -> "Unexpected exception: " + e.Message caught |> equal "Value cannot be null. (Parameter 'arg')" + +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", box 5, "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.exactlyOne [ 1; 2 ] |> ignore) |> equal "source" + paramName (fun () -> Seq.head Seq.empty |> ignore) |> equal "source" + paramName (fun () -> System.Array.BinarySearch([| 1; 3; 5 |], -1, 2, 5) |> ignore) |> equal "index" + paramName (fun () -> Array.sub [| 1 |] 0 5 |> ignore) |> equal "count" + paramName (fun () -> Array.sub [| 1 |] -1 1 |> ignore) |> equal "startIndex" + +[] +let ``test ArgumentException parameter name is evaluated once`` () = + let calls = ref 0 + let name () = + calls.Value <- calls.Value + 1 + "x" + paramName (fun () -> invalidArg (name ()) "m") |> equal "x" + paramName (fun () -> nullArg (name ())) |> equal "x" + System.ArgumentException("m", name ()).ParamName |> equal "x" + System.ArgumentNullException(name (), "m").ParamName |> equal "x" + calls.Value |> equal 4 diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index fb0c2a645e..e532689428 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -63,6 +63,12 @@ type ElmishWidget<'Model, 'View, 'Msg>(view: 'Model -> ('Msg -> unit) -> 'View) let _view = view member _.Render(model, dispatch) = _view model dispatch +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests() = testCase "ref works" <| fun () -> let x = ref 5 @@ -164,6 +170,27 @@ let tests() = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + // Dart strings are not nullable, so an absent name reads as an empty string + System.ArgumentException("m").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", exn "i").ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> System.String.IsNullOrEmpty |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> System.String.IsNullOrEmpty |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 6df4c81f2c..94153e6d16 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -510,6 +510,13 @@ let buildRecordWithMutableFnWithFor (xs: RecordWithMutableFn list) = let x = xs[i] items <- (x.MutableFn >>* ()) :: items items + +let paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -1269,6 +1276,26 @@ let tests = | e -> "Unexpected exception: " + e.Message equal "This is invalid (Parameter 'arg')" caught + testCase "ArgumentException.ParamName works" <| fun () -> + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + + testCase "ArgumentException.ParamName of FSharp.Core errors works" <| fun () -> + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + testCase "invalidOp raises InvalidOperationException" <| fun () -> let caught = try diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index 0fe681a1f1..ab47fc7125 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1287,6 +1287,34 @@ let ``test invalidArg raises ArgumentException`` () = | e -> "Unexpected exception: " + e.Message caught |> equal "This is invalid (Parameter 'arg')" +let private paramName (f: unit -> unit) = + try + f () + "no exception" + with :? System.ArgumentException as e -> e.ParamName + +[] +let ``test ArgumentException.ParamName works`` () = + paramName (fun () -> invalidArg "x" "m") |> equal "x" + paramName (fun () -> nullArg "x") |> equal "x" + System.ArgumentException("m", "x").ParamName |> equal "x" + System.ArgumentException("m").ParamName |> isNull |> equal true + System.ArgumentException().ParamName |> isNull |> equal true + System.ArgumentException("m", exn "i").ParamName |> isNull |> equal true + System.ArgumentException("m", "x", exn "i").ParamName |> equal "x" + System.ArgumentNullException("x").ParamName |> equal "x" + System.ArgumentNullException("x", "m").ParamName |> equal "x" + System.ArgumentNullException().ParamName |> isNull |> equal true + System.ArgumentOutOfRangeException("x").ParamName |> equal "x" + System.ArgumentOutOfRangeException("x", "m").ParamName |> equal "x" + System.ArgumentOutOfRangeException().ParamName |> isNull |> equal true + +[] +let ``test ArgumentException.ParamName of FSharp.Core errors works`` () = + paramName (fun () -> List.head ([]: int list) |> ignore) |> equal "list" + paramName (fun () -> List.take -1 [ 1 ] |> ignore) |> equal "count" + paramName (fun () -> List.reduce (+) ([]: int list) |> ignore) |> equal "list" + [] let ``test invalidOp raises InvalidOperationException`` () = let caught = From 5521d3892a52cd39f25107b2f4d713da64e4d2ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:04:12 +0100 Subject: [PATCH 272/322] chore: Bump the all-nuget group with 7 updates (#5207) Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 4 ++-- src/Directory.Build.props | 2 +- src/Fable.Build/Fable.Build.fsproj | 2 +- src/Fable.Cli/Fable.Cli.fsproj | 2 +- src/Fable.Compiler/Fable.Compiler.fsproj | 2 +- src/fable-standalone/src/Worker/Worker.fsproj | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 43464f190f..d6c3b40842 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.0.10", + "version": "8.1.0", "commands": [ "fantomas" ], @@ -24,7 +24,7 @@ "rollForward": false }, "easybuild.shipit": { - "version": "3.2.0", + "version": "3.3.0", "commands": [ "shipit" ], diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 869b16c3f7..11646642b7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -23,7 +23,7 @@ - + all build diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index 326ea23a0e..0999bbae27 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -49,7 +49,7 @@ - + diff --git a/src/Fable.Cli/Fable.Cli.fsproj b/src/Fable.Cli/Fable.Cli.fsproj index d1e50711e2..b86fb6ebea 100644 --- a/src/Fable.Cli/Fable.Cli.fsproj +++ b/src/Fable.Cli/Fable.Cli.fsproj @@ -58,7 +58,7 @@ all - + diff --git a/src/Fable.Compiler/Fable.Compiler.fsproj b/src/Fable.Compiler/Fable.Compiler.fsproj index fc0b532aca..c9a64ab38c 100644 --- a/src/Fable.Compiler/Fable.Compiler.fsproj +++ b/src/Fable.Compiler/Fable.Compiler.fsproj @@ -43,7 +43,7 @@ - + diff --git a/src/fable-standalone/src/Worker/Worker.fsproj b/src/fable-standalone/src/Worker/Worker.fsproj index a9ce5f3a3a..00eb08cd09 100644 --- a/src/fable-standalone/src/Worker/Worker.fsproj +++ b/src/fable-standalone/src/Worker/Worker.fsproj @@ -11,7 +11,7 @@ - + From 4508887a359fa897ab37fc40161b5b486ef1815e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:04:15 +0100 Subject: [PATCH 273/322] chore: Bump the all-actions group across 1 directory with 3 updates (#5122) Signed-off-by: dependabot[bot] --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index bdd7390d07..e80a8348c3 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup-cli@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index d5ff7a08fe..f82d9afac5 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1371,7 +1371,7 @@ jobs: GH_AW_INFO_VERSION: "1.0.40" - name: Generate GitHub App token id: safe-outputs-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: client-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@866158eb5c6cbfd3c614104e3d3d6634e246d220 # v0.91.8 + uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1928,7 +1928,7 @@ jobs: path: /tmp/gh-aw/ - name: Generate GitHub App token id: safe-outputs-app-token - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: client-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} From 225bc732d474930107582052d1b0d22351f7712d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:04:23 +0100 Subject: [PATCH 274/322] chore: Bump the all-pip group with 2 updates (#5206) Signed-off-by: dependabot[bot] --- pyproject.toml | 4 +-- uv.lock | 82 +++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2eb5700318..4cee6e5f80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,13 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ "pytest>=9.1.1,<10", - "ruff>=0.17.1,<0.18", + "ruff>=0.17.2,<0.18", "maturin>=1.15.0,<2", "dunamai>=1.26.3,<2", "pydantic>=2.14.1", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.89", + "ty>=0.0.91", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 0cd456fa7a..3a7a74917a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.12, <4.0" [[package]] @@ -68,8 +68,8 @@ dev = [ { name = "pydantic", specifier = ">=2.14.1" }, { name = "pyright", specifier = ">=1.1.414" }, { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.17.1,<0.18" }, - { name = "ty", specifier = ">=0.0.89" }, + { name = "ruff", specifier = ">=0.17.2,<0.18" }, + { name = "ty", specifier = ">=0.0.91" }, ] [[package]] @@ -290,52 +290,52 @@ wheels = [ [[package]] name = "ruff" -version = "0.17.1" +version = "0.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/58/690cf56da80c8f2c8bf12042b6017beac2c34fe961f65420bcb3fb7fac38/ruff-0.17.2.tar.gz", hash = "sha256:84d632402d14c23b488f941db23712d6736a013225162062d34558c3f3c780eb", size = 5333400, upload-time = "2026-10-22T18:38:00.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, - { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, - { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, - { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, - { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, + { url = "https://files.pythonhosted.org/packages/98/b8/8b6d69c57e8fe1cc6af1aa7493b72a1a5b5a8afe34694e302025441729d4/ruff-0.17.2-py3-none-linux_armv6l.whl", hash = "sha256:96a65e755d8dba094707fb23ab602111d9a31ec2835b3a6992a1914a8fcb211e", size = 10183857, upload-time = "2026-10-22T18:37:14.956Z" }, + { url = "https://files.pythonhosted.org/packages/e8/61/12cc2ff7b2fa90a268ce1e2162c23aaf76e601d22623f6a31b0eeaa764b0/ruff-0.17.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:39c5b55c960515cdb32be16e69efb5734c589c06a0eb38a22694524a336b889d", size = 10210904, upload-time = "2026-10-22T18:37:17.985Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7c/b8850a8e4ad29f64a827dd6c6902db4a9aa538fb2b9eef4e26084001a3bb/ruff-0.17.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:92a80d2f44584dabb0189b3a6c393e37e525e04942838482010575cb81945be7", size = 9408439, upload-time = "2026-10-22T18:37:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b6/6a64fae86ca5e5e29d5adc311e5481de726cedf9aee607435a16c31c442a/ruff-0.17.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7d0906677d89bbf3cf73cde64b5bbe2ab35df3ae43e49ee2627301e91265bac", size = 10047078, upload-time = "2026-10-22T18:37:23.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/19/23d376547715175aa1bd46df48df6aacc7c6439d9daf19cac15975180aa5/ruff-0.17.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cfd783590edf2b77317882631900f6a13ea2e8eacefe6ebf2370d2715299160", size = 9990478, upload-time = "2026-10-22T18:37:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/93/6c/34cda32cbe13adac5c6b291a307a5570d8ebd37c0c86339f0fe628e55632/ruff-0.17.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122258f6ecde0e0dd1bb60b24263c3edde4338caf723fca711cc729f9bfd414a", size = 10673624, upload-time = "2026-10-22T18:37:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/5d762a4dbc851d8bb47863cf3a28a61bf31c804d393b70d891fac899f5d1/ruff-0.17.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8fba429d6ac4128ab29cfb8fa6240d2fa671d04adf7f84d3b22fc7b6b6bea4", size = 11351797, upload-time = "2026-10-22T18:37:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/3f/04/a0e92a81faf5c81c1c98d4374c8401734ccf2b8cca7d0170cb0fec08c6e2/ruff-0.17.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bd67bb9e7061dd09bc46a0a884bae2bbd214dbcd48e509813612619683869", size = 10949414, upload-time = "2026-10-22T18:37:35.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d8/59894cf630d2d86882457a2a6a0a1533410c5c805bfbac6f6eb4b5aef69d/ruff-0.17.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60f93fec7142dba5b8f487f94bf2f6231dae24dc11e4c48648347c8b0720004", size = 10528000, upload-time = "2026-10-22T18:37:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/ab/95/d6e30c51e851d37e10762ad91fa1e76ddaad998589b5667962a3cfbc9edd/ruff-0.17.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c555a877f8f41447e318101cfefea9bf12dfa9f82eedf90c376e57e069f917d0", size = 10571779, upload-time = "2026-10-22T18:37:40.444Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5a/a7e1d74fb464250137d3d4eca645d323536533b672004d04ee0e46e0caf3/ruff-0.17.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ba405cbd5ec7bda1f555d5e52be1fd68d02608d4d95d24591bdac92cfacb579f", size = 10051707, upload-time = "2026-10-22T18:37:42.812Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f2/02fca29ca7999380f0350a7212fe755c11ab9c22d61327356cce8decdbc6/ruff-0.17.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c21b4ab24485358f17291a4f8dc5399b834e963b8efedd920fe4fdb02081bb4", size = 9998227, upload-time = "2026-10-22T18:37:45.224Z" }, + { url = "https://files.pythonhosted.org/packages/d1/00/55aa604a093dfdc1526e570fae0bb7d37a2221c7b4728d5f080b2add16ba/ruff-0.17.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5875a16b0de70b43cb81eabea5ae5e8f5d1b2fd1b5e6ed07f80cfe5019d0bd5a", size = 10377692, upload-time = "2026-10-22T18:37:47.704Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5a/a7a640f47bb906ec8fa233ea84319d503e6dee02b59e3901bd312ebd1f9b/ruff-0.17.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d086be452eed686aa51271ecf2cbe5037956517cc6ee39a17b4489ac12f8bbe", size = 10790587, upload-time = "2026-10-22T18:37:50.191Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d4/c3b74e1b4808438ea7f14bb914478025e2d51261f60774a54a0fa222ec46/ruff-0.17.2-py3-none-win32.whl", hash = "sha256:6bce7d1f5165701ae18f5df85c65afe78cddfa6c5fb4b9bdb0014d60388f3edd", size = 9870537, upload-time = "2026-10-22T18:37:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/c7da96c2c96d20704cf9027f7a5121da272a8c05f1d1531ba949e3b3df04/ruff-0.17.2-py3-none-win_amd64.whl", hash = "sha256:1b59d447f197bef9ef48a22ba1a6016137753c6099e9bac9c36eb566e422424c", size = 10282826, upload-time = "2026-10-22T18:37:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/81/60/a3604c79cb02777b6cb84da894104021986f06c4bc41375102cc95049a32/ruff-0.17.2-py3-none-win_arm64.whl", hash = "sha256:195ad918027f817c119310ca89a42d347887dfc28934aaa869325e9c5dc9d962", size = 9827036, upload-time = "2026-10-22T18:37:58.031Z" }, ] [[package]] name = "ty" -version = "0.0.89" +version = "0.0.91" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz", hash = "sha256:bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4", size = 7978241, upload-time = "2026-10-16T10:35:05.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/c0/9e26817a5c94d504aab30bffbbf53f836f4763dd0a947089246ff555eebe/ty-0.0.91.tar.gz", hash = "sha256:b9c434cc1e3b106b6b9353b78ef304d5904c82cf440abcafef7134270a22b41a", size = 8035848, upload-time = "2026-10-22T18:24:52.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/84/e199d9a3dc3c94c804ddd9e94b63dbe543ae7f39329574963b6a6a95872f/ty-0.0.89-py3-none-linux_armv6l.whl", hash = "sha256:739b087e8873e177ffda99ff8a3600a4a7704d06523a8e88c4493021eee40b8a", size = 14254879, upload-time = "2026-10-16T10:34:24.813Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/5afe09bf5c83c4e3a9e5913386a460e240a7029f2ae5fc96df5b43563a91/ty-0.0.89-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:47ebc98480c8a301955e18a8bdd67ef07090071ad47349b3254f325d3d5ab4e7", size = 13790417, upload-time = "2026-10-16T10:34:27.672Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f9/88648f0ba9400dfd642c1e8dc0aeecb87945b121f3ff51f3902883c016d6/ty-0.0.89-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0631291af5c850a15007c0ffb906d813966466e4ec12173c44977c0c5a2d51d", size = 12856900, upload-time = "2026-10-16T10:34:29.871Z" }, - { url = "https://files.pythonhosted.org/packages/6a/89/baae2b407101c4cb02f19216123900c147af8fd87cdb1ed7eba139472734/ty-0.0.89-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d768300ae16274ce656006e9199869ad68684b74400d963e1c82deddf3a22ad", size = 13653330, upload-time = "2026-10-16T10:34:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/293516c2e10a77134cfc40d22c3fc08edd8c5eadae9c75531ec8a9263348/ty-0.0.89-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dd4c57ba58bab8f6c816b2c254db52b58834b5dbf8268287382f7a2f9735c8f", size = 13986215, upload-time = "2026-10-16T10:34:34.506Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c3/da911c8fc22d2664692432570619576b25c7cd0a586401cab08987763094/ty-0.0.89-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1af774c24117d366b69142d4f35edee518617f8b926d13560d0334cbe5aa3f85", size = 14905008, upload-time = "2026-10-16T10:34:36.632Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c3/aed8d920b9bbf7b582e9fc0305acb2f2c748ede788e7dba9d6952e939c78/ty-0.0.89-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8147811b9b3bc2dc9338cd01c7893ac931fc3fae7aace169047ecbd627e6628", size = 15278217, upload-time = "2026-10-16T10:34:39.144Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/c6b4320f08fca44ffe279e5ddeb3eae12869c6341f596931accdefea1182/ty-0.0.89-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d5f178c91d50fb62461b5e579028b7d2e92872e1d8f02e96ace26464c2277a6", size = 15206289, upload-time = "2026-10-16T10:34:41.626Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc", size = 14510395, upload-time = "2026-10-16T10:34:44.063Z" }, - { url = "https://files.pythonhosted.org/packages/f0/32/5f9ce2377a77ad9ff36d8996edf86b1f069c1e8716e37c7d6d668402b110/ty-0.0.89-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a70ab1a402746d7837407dc169e461fdf90945bc1eef0d3c0f1be543490060bf", size = 14767366, upload-time = "2026-10-16T10:34:46.154Z" }, - { url = "https://files.pythonhosted.org/packages/63/72/2b28c71d6ebd97b5bb1378f1806598c5dd90278f7b99a431dd3879dddcfc/ty-0.0.89-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b55c28a49bd2676eac9b3540a1dcc4cc053e4671c65dcec9fdad3dfe55fb148c", size = 13670284, upload-time = "2026-10-16T10:34:48.225Z" }, - { url = "https://files.pythonhosted.org/packages/34/cc/68b8ebf98c9bc582fddc3d2b93f6e0f53719d1ddee1c5037dfa2e1cb8467/ty-0.0.89-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c9f22edeb8176f8ab189337e8cbd8f92b66744534a2886df9e252b52ade991c", size = 13997891, upload-time = "2026-10-16T10:34:50.556Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4d/a1d61e06092fd5726f48a8711bd32eac6af132308eb222a0027caba4f42d/ty-0.0.89-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c186185cad57a1469b851096b51345de7b625e353c1f4acd338402daed22fa7", size = 14326247, upload-time = "2026-10-16T10:34:53.005Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d2/466d3762e330504a2c114dbbd0f0383229f9652dccd56277dde008bfe0da/ty-0.0.89-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5e38302ac218d2404758241093b5542ed0345d4f569096589f1810034244e998", size = 14661527, upload-time = "2026-10-16T10:34:55.555Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d5/dfd7ed5e4af472b20a11ef651da4bca55f9dc8be485cd152337f94caddde/ty-0.0.89-py3-none-win32.whl", hash = "sha256:0146637ed2321717280bfe4e4b20a18813b741da280e8039d7df2bf22b0251f1", size = 13224078, upload-time = "2026-10-16T10:34:57.735Z" }, - { url = "https://files.pythonhosted.org/packages/b4/14/1ee663c422e3a2d910c5cc27e7d84bd7709a3df0aca28a713843c6c3fa5d/ty-0.0.89-py3-none-win_amd64.whl", hash = "sha256:9fc56322a1aff29a19088d2025f3c42a6d25a7035668048bb908c9f8b66f480d", size = 13727433, upload-time = "2026-10-16T10:35:00.15Z" }, - { url = "https://files.pythonhosted.org/packages/09/93/af8f7250920823ca6d582bcf0abf803fb92c8b99415ba7f40f2304102c6f/ty-0.0.89-py3-none-win_arm64.whl", hash = "sha256:1d4967c8f7bfd67c9708115b41d7049197c5d20c76fb2e136fe78b0437ada05b", size = 13287101, upload-time = "2026-10-16T10:35:02.918Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/0a850c1ca8e6e802e6b0f730f0d59c9769845b2fc956e3acd7f429b665f2/ty-0.0.91-py3-none-linux_armv6l.whl", hash = "sha256:5d39e6e485854838eb7fcdb747495f5b90283d58bce869c17445120fe0c7e40a", size = 14255787, upload-time = "2026-10-22T18:24:11.284Z" }, + { url = "https://files.pythonhosted.org/packages/33/87/9ccfa2d97b8b043e4d6fab10febc50ac7856b02cf7cd64910085db806bbe/ty-0.0.91-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcf953e25f90fbccf6cc27ef43fc4ef0961c89f54866e32cd1d7f24891b96084", size = 13822267, upload-time = "2026-10-22T18:24:14.31Z" }, + { url = "https://files.pythonhosted.org/packages/13/33/be8f62444c7840d1b0928c6d174540c0fe6b1b193ae9324c8249e8ed52b9/ty-0.0.91-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78e3e66e88a9ae903afe0981ff9bd225d452ade2aeb070715842510dfcbf6de4", size = 12957620, upload-time = "2026-10-22T18:24:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/a742f273c065c4f5a054cc2b44076207136ff1cd346adfb23da38d8894d8/ty-0.0.91-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ecaf87b3d7b508f9ea1ef8715d4cf47942719659028c92fad819d82a82ccfa", size = 13746018, upload-time = "2026-10-22T18:24:19.221Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a0/3eecf8717cbe4e05d6e02b53679aebad79f1bfcff6c9c03d7601cb7df5f9/ty-0.0.91-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ec8fe4243aab445abece38a2b8e9b3010230d5e6b3f4e15bfa811a914d99e73", size = 13993220, upload-time = "2026-10-22T18:24:21.328Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/fb3edee2857e97d85b6c735901469fd523cf6792382e4fbbb00b065c186c/ty-0.0.91-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:577701ec2a9cf7a50f6059069399b9437bb8af7a99df24012776e95d8a55831f", size = 14936911, upload-time = "2026-10-22T18:24:24.393Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2c/82f29ee86f4171807af8ab2feaf4e51211143118a3e53e9da2c2e77eca7d/ty-0.0.91-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc683f49197d63ec47b2eaa9763ddb32dafb2a31190d938b9bb8ebecc5b25173", size = 15336384, upload-time = "2026-10-22T18:24:26.472Z" }, + { url = "https://files.pythonhosted.org/packages/0b/85/e2419b19577ead84b8afa9137c12eb4bde19f926423bfe01e615db12c52c/ty-0.0.91-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8bb2784572fa22f77e417844936f6a9f831a6b0ba64749ca1065c407aefcd23", size = 15332319, upload-time = "2026-10-22T18:24:28.979Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/dc28cea0796ee0268bf987d1d6f0f168605bec8898dbdbc7676049d2667b/ty-0.0.91-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68bb6bfc8171910335a47c7e84070f28bbbc0326bcd6ef08fe3690d0509556b", size = 14600250, upload-time = "2026-10-22T18:24:31.359Z" }, + { url = "https://files.pythonhosted.org/packages/1e/71/83dd28e628698059ba6fe7081acafa7a8f65cfd90fe6573907ca8219d561/ty-0.0.91-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63f268c80697d614d18e988476d2517177d66ec732ae104d86e57b60f197a416", size = 14812339, upload-time = "2026-10-22T18:24:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/63c0eae2d5b02cef98a9b327b1a16b36287c140b3a7d2306ff7560177f9c/ty-0.0.91-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b0f34630aa92e222d7754d66222d116f027025cb2b9ed66e1b4b73623de74e84", size = 13703060, upload-time = "2026-10-22T18:24:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/fd6d20d00202949d775043c5f70679f38d0e48ea30eeacbffaf6e0a9754e/ty-0.0.91-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e62f8bbc17d481e01cce39bdfbe36beefda0478cb210e8045ef618d3f674a731", size = 14006236, upload-time = "2026-10-22T18:24:38.464Z" }, + { url = "https://files.pythonhosted.org/packages/fa/26/53dddd4f927dfaa73ea14cd71d97b4d92d685e365b5b037bac457e271403/ty-0.0.91-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f8bf4514a383b13d7cf86eddb55f0eb9f356fc7efeae0de714ad4fe5fc54fee8", size = 14356612, upload-time = "2026-10-22T18:24:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/89/40/ccebba145dd244b2f6058ce2e4a6ff34dea36d4a5b38dd7a6ae983b65a3b/ty-0.0.91-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a6d93ab988c4c5ed92b52389b888d46ddede78e4646fe28db615bbeff89cdf82", size = 14674843, upload-time = "2026-10-22T18:24:43.325Z" }, + { url = "https://files.pythonhosted.org/packages/08/6f/8a7701adf1f539a893274358f12ba62773d5bb40920bfc53e0d852bfd576/ty-0.0.91-py3-none-win32.whl", hash = "sha256:b9f4bba8486a3d13babc11f8115369d0a0dc8bef127b7cf4f4c5eed9d4dbfa01", size = 13260545, upload-time = "2026-10-22T18:24:45.715Z" }, + { url = "https://files.pythonhosted.org/packages/fc/63/27c8051a72328ee81f47dace5e2c63e7db43bcae935db25a9dbd4d0e59b2/ty-0.0.91-py3-none-win_amd64.whl", hash = "sha256:5ac9eaf938e7fba4dd6c3358f852e6eb3127aa7bde0a76d9807c41f60ad73a29", size = 13759484, upload-time = "2026-10-22T18:24:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a9/1f6c3694b7b33be4ae9a272f71a99a7dd27ec020c32a22049e9c0e79f576/ty-0.0.91-py3-none-win_arm64.whl", hash = "sha256:35820957639db1fb8c643273670967192c308038836005bdf3bfef111aff64a0", size = 13318343, upload-time = "2026-10-22T18:24:50.116Z" }, ] [[package]] From 24d9c41ad69993f0c4e843558133bcd787a960d4 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Thu, 29 Oct 2026 12:07:38 -0700 Subject: [PATCH 275/322] feat(js/ts): support the "R" numeric format and choose "G" notation like .NET (#5212) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Replacements.fs | 31 ++++---- src/fable-library-ts/String.ts | 65 ++++++++++++++-- tests/Js/Main/ConvertTests.fs | 108 ++++++++++++++++++++++++++- 3 files changed, 180 insertions(+), 24 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 9a930256d5..b3128c53fc 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2698,18 +2698,19 @@ let parseBool (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> None let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) format = - let libCallFormat () = + let libCallFormat (token: string option) = let format = emitExpr r String [ format ] "'{0:' + $0 + '}'" + let thisArg = thisArg.Value - Helper.LibCall( - com, - "String", - "format", - t, - [ format; thisArg.Value ], - [ format.Type; thisArg.Value.Type ], - ?loc = r - ) + let meth, thisArg = + match thisArg.Type, token |> Option.map (fun token -> token.ToLower()) with + | Number(Float32, _), (None | Some("g" | "r")) -> "formatSingle", thisArg + // "G" and "R" would print the sign of an int that JS holds as -0 + | Number(Integers _, _), (None | Some("g" | "r")) -> + "format", makeBinOp r thisArg.Type thisArg (makeIntConst 0) BinaryPlus + | _ -> "format", thisArg + + Helper.LibCall(com, "String", meth, t, [ format; thisArg ], [ format.Type; thisArg.Type ], ?loc = r) |> Some match format with @@ -2747,8 +2748,8 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA | "f" | "g" | "n" - | "p" -> None - | "r" -> "with round-trip format specifier is not support by Fable" |> Some + | "p" + | "r" -> None | "x" -> match numberKind with | Integers _ @@ -2762,11 +2763,11 @@ let numericStringFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisA |> addError com ctx.InlinePath r None - | None -> libCallFormat () + | None -> libCallFormat (Some token) else - libCallFormat () + libCallFormat (Some format) - | _ -> libCallFormat () + | _ -> libCallFormat None let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index eaf533c0a3..d12d60b696 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -363,6 +363,56 @@ function thousandSeparate(value: string) { return value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } +function splitExponential(s: string): [string, number] { + const eIdx = s.indexOf("e"); + return [s.slice(0, eIdx).replace(".", ""), Number(s.slice(eIdx + 1))]; +} + +function shortestSingle(x: number): [string, number] { + // 100 digits tell where a float32 lies between two candidates, and 9 digits always round-trip + const [digits, exponent] = splitExponential(x.toExponential(99)); + for (let p = 1; ; p++) { + const down = Number(digits.slice(0, p)); + const rest = digits.slice(p); + const half = "5".padEnd(rest.length, "0"); + const roundUp = rest > half || rest === half && down % 2 === 1; + for (const n of roundUp ? [down + 1, down] : [down, down + 1]) { + if (Math.fround(Number(n + "e" + (exponent - p + 1))) === x) { + return [String(n), exponent + String(n).length - p]; + } + } + } +} + +// .NET "G" and "R": fixed notation unless the exponent is below -4 or reaches the precision (17 for the shortest digits, 9 for a float32) +function formatGeneral(x: number, format: string, precision: number | null, isSingle = false) { + if (!Number.isFinite(x)) { + return String(x); + } + const sign = x < 0 || Object.is(x, -0) ? "-" : ""; + x = Math.abs(x); + // R ignores the precision and G0 is G + const p = format === "r" || format === "R" ? 0 : precision ?? 0; + const [significand, exponent] = + p > 0 ? splitExponential(x.toExponential(p - 1)) : isSingle ? shortestSingle(x) : splitExponential(x.toExponential()); + const digits = trimEnd(significand, "0") || "0"; + if (exponent < -4 || exponent >= (p > 0 ? p : isSingle ? 9 : 17)) { + const mantissa = digits.length > 1 ? digits[0] + "." + digits.slice(1) : digits; + const eChar = format === "G" || format === "R" ? "E" : "e"; + return sign + mantissa + eChar + (exponent < 0 ? "-" : "+") + padLeft(String(Math.abs(exponent)), 2, "0"); + } else if (exponent < 0) { + return sign + "0." + "0".repeat(-exponent - 1) + digits; + } else { + const decimals = digits.slice(exponent + 1); + return sign + digits.slice(0, exponent + 1).padEnd(exponent + 1, "0") + (decimals ? "." + decimals : ""); + } +} + +export function formatSingle(str: string, x: number) { + const m = /^\{0:([GgRr])(\d{0,2})\}$/.exec(str); + return m != null ? formatGeneral(Math.fround(x), m[1], m[2] ? parseInt(m[2], 10) : null, true) : format(str, x); +} + export function format(str: string | object, ...args: any[]) { let str2: string; if (typeof str === "object") { @@ -434,12 +484,17 @@ export function format(str: string | object, ...args: any[]) { rep = parts.integral + "." + padRight(parts.decimal, precision, "0"); } break; - case "g": case "G": { - if (precision == null && typeof rep !== "number" && typeof rep !== "bigint") { + case "g": case "G": + case "r": case "R": { + if (typeof rep === "number") { + rep = formatGeneral(rep, format, precision); + break; + } + if (precision == null) { rep = String(rep); break; } - rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); + rep = toPrecision(rep, precision); // Handle exponential notation: only trim trailing zeros from mantissa, not from exponent. // .NET G format guarantees an exponent of at least 2 digits with an explicit sign (e.g. "E-07"). const eIdx = rep.indexOf("e"); @@ -448,7 +503,7 @@ export function format(str: string | object, ...args: any[]) { const expSign = rep[eIdx + 1]; // toPrecision always emits "+" or "-" const expDigits = rep.slice(eIdx + 2); const paddedExpDigits = expDigits.length < 2 ? "0" + expDigits : expDigits; - const eChar = format === "G" ? "E" : "e"; + const eChar = format === "G" || format === "R" ? "E" : "e"; rep = mantissa + eChar + expSign + paddedExpDigits; } else if (rep.indexOf(".") >= 0) { rep = trimEnd(trimEnd(rep, "0"), "."); @@ -475,8 +530,6 @@ export function format(str: string | object, ...args: any[]) { rep = thousandSeparate(parts.integral) + " %"; } break; - case "r": case "R": - throw new Exception("The round-trip format is not supported by Fable"); case "x": case "X": if (!isIntegral(rep)) { throw new Exception("Format specifier was invalid."); diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 9ef1fb71a3..bf7cb88013 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -37,6 +37,9 @@ let private toPlainArray (bytes: byte[]) : byte[] = bytes #endif +// Computed, since the standalone compiler writes a -0. literal as 0 +let private negZero = 1. / Double.NegativeInfinity + let private adaptExponentValue (value: string) = value.ToLowerInvariant().Replace("e+00", "e+").Replace("e+0", "e+") @@ -1054,6 +1057,11 @@ let tests = |> adaptExponentValue |> equal "2.4e+8" + // JS holds this int as -0 + [ -("0".Length - 1) ] + |> List.map (fun x -> x.ToString("G", CultureInfo.InvariantCulture), x.ToString("g5", CultureInfo.InvariantCulture)) + |> equal [ ("0", "0") ] + testCase "uint32.ToString 'G' works" <| fun () -> (255u).ToString("G2", CultureInfo.InvariantCulture) @@ -1112,9 +1120,40 @@ let tests = |> adaptExponentValue |> equal "255.24" - // (-255.2357f).ToString("G", CultureInfo.InvariantCulture) - // |> adaptExponentValue - // |> equal "-255.2357" + (-255.2357f).ToString("G", CultureInfo.InvariantCulture) + |> adaptExponentValue + |> equal "-255.2357" + + testCase "float.ToString 'G' chooses the notation like .NET" + <| fun () -> + (1e-4).ToString("G", CultureInfo.InvariantCulture) |> equal "0.0001" + (1.5e-5).ToString("G", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("g", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1e16).ToString("G", CultureInfo.InvariantCulture) |> equal "10000000000000000" + (1e17).ToString("G", CultureInfo.InvariantCulture) |> equal "1E+17" + (123456789012345678901.0).ToString("G", CultureInfo.InvariantCulture) |> equal "1.2345678901234568E+20" + negZero.ToString("G", CultureInfo.InvariantCulture) |> equal "-0" + (123.456).ToString("G0", CultureInfo.InvariantCulture) |> equal "123.456" + (1.5e-5).ToString("G3", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("G17", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (5e-5).ToString("G17", CultureInfo.InvariantCulture) |> equal "5.0000000000000002E-05" + negZero.ToString("G17", CultureInfo.InvariantCulture) |> equal "-0" + (1e16).ToString("G16", CultureInfo.InvariantCulture) |> equal "1E+16" + (1e17).ToString("G20", CultureInfo.InvariantCulture) |> equal "100000000000000000" + (0.1 + 0.2).ToString("G20", CultureInfo.InvariantCulture) |> equal "0.30000000000000004441" + $"{1.5e-5:G}" |> equal "1.5E-05" + String.Format(CultureInfo.InvariantCulture, "{0:g}", 1e17) |> equal "1e+17" + + testCase "float32.ToString 'G' chooses the digits and notation like .NET" + <| fun () -> + (0.1f).ToString("G", CultureInfo.InvariantCulture) |> equal "0.1" + (float32 negZero).ToString("G", CultureInfo.InvariantCulture) |> equal "-0" + (1.5e-5f).ToString("G", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1e8f).ToString("G", CultureInfo.InvariantCulture) |> equal "100000000" + (1e9f).ToString("g", CultureInfo.InvariantCulture) |> equal "1e+09" + (123456789.f).ToString("G", CultureInfo.InvariantCulture) |> equal "123456790" + (0.1f).ToString("G9", CultureInfo.InvariantCulture) |> equal "0.100000001" + (1e8f).ToString("G5", CultureInfo.InvariantCulture) |> equal "1E+08" testCase "ToString 'G' keeps the trailing zeros of the integer part" <| fun () -> @@ -1364,6 +1403,69 @@ let tests = (-255.2357f).ToString("P", CultureInfo.InvariantCulture) |> equal "-25,523.57 %" + testCase "float.ToString 'R' works" + <| fun () -> + (0.).ToString("R", CultureInfo.InvariantCulture) |> equal "0" + negZero.ToString("R", CultureInfo.InvariantCulture) |> equal "-0" + (1.5).ToString("R") |> equal "1.5" + (0.1 + 0.2).ToString("R", CultureInfo.InvariantCulture) |> equal "0.30000000000000004" + (1e-4).ToString("R", CultureInfo.InvariantCulture) |> equal "0.0001" + (1.5e-5).ToString("R", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1.5e-5).ToString("r", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1.5e-5).ToString("R3", CultureInfo.InvariantCulture) |> equal "1.5E-05" + (1e-7).ToString("R", CultureInfo.InvariantCulture) |> equal "1E-07" + (1e15).ToString("R", CultureInfo.InvariantCulture) |> equal "1000000000000000" + (1e16).ToString("R", CultureInfo.InvariantCulture) |> equal "10000000000000000" + (1e17).ToString("R", CultureInfo.InvariantCulture) |> equal "1E+17" + (12345678901234567.0).ToString("R", CultureInfo.InvariantCulture) |> equal "12345678901234568" + (123456789012345678901.0).ToString("R", CultureInfo.InvariantCulture) |> equal "1.2345678901234568E+20" + (1e21).ToString("r", CultureInfo.InvariantCulture) |> equal "1e+21" + Double.Epsilon.ToString("R", CultureInfo.InvariantCulture) |> equal "5E-324" + Double.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "1.7976931348623157E+308" + Double.MinValue.ToString("R", CultureInfo.InvariantCulture) |> equal "-1.7976931348623157E+308" + nan.ToString("R", CultureInfo.InvariantCulture) |> equal "NaN" + infinity.ToString("R", CultureInfo.InvariantCulture) |> equal "Infinity" + (-infinity).ToString("R", CultureInfo.InvariantCulture) |> equal "-Infinity" + + testCase "float32.ToString 'R' works" + <| fun () -> + (0.1f).ToString("R", CultureInfo.InvariantCulture) |> equal "0.1" + (0.1f + 0.2f).ToString("R", CultureInfo.InvariantCulture) |> equal "0.3" + (float32 negZero).ToString("R", CultureInfo.InvariantCulture) |> equal "-0" + (1.5e-5f).ToString("r", CultureInfo.InvariantCulture) |> equal "1.5e-05" + (1e8f).ToString("R", CultureInfo.InvariantCulture) |> equal "100000000" + (1e9f).ToString("R", CultureInfo.InvariantCulture) |> equal "1E+09" + (123456789.f).ToString("R", CultureInfo.InvariantCulture) |> equal "123456790" + (1234.5678f).ToString("R", CultureInfo.InvariantCulture) |> equal "1234.5677" + (-306242.625f).ToString("R", CultureInfo.InvariantCulture) |> equal "-306242.62" + (1.2621775e-29f).ToString("R", CultureInfo.InvariantCulture) |> equal "1.2621775E-29" + Single.Epsilon.ToString("R", CultureInfo.InvariantCulture) |> equal "1E-45" + Single.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "3.4028235E+38" + nanf.ToString("R", CultureInfo.InvariantCulture) |> equal "NaN" + + testCase "ToString 'R' works for integers and decimals" + <| fun () -> + (5).ToString("R", CultureInfo.InvariantCulture) |> equal "5" + (-2147483648).ToString("r", CultureInfo.InvariantCulture) |> equal "-2147483648" + (5uy).ToString("R", CultureInfo.InvariantCulture) |> equal "5" + Int64.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "9223372036854775807" + UInt64.MaxValue.ToString("R", CultureInfo.InvariantCulture) |> equal "18446744073709551615" + (1.50M).ToString("R", CultureInfo.InvariantCulture) |> equal "1.50" + (1.2345M).ToString("R3", CultureInfo.InvariantCulture) |> equal "1.23" + // JS holds this int as -0 + [ -("0".Length - 1) ] |> List.map (fun x -> x.ToString("R", CultureInfo.InvariantCulture)) |> equal [ "0" ] + + testCase "String.Format and interpolation support 'R'" + <| fun () -> + String.Format("{0:R}", 1.5e-5) |> equal "1.5E-05" + String.Format(CultureInfo.InvariantCulture, "{0:r}", negZero) |> equal "-0" + String.Format(CultureInfo.InvariantCulture, "{0,8:R}", 1.5) |> equal " 1.5" + String.Format("{0:R}", 12345678901234567890I) |> equal "12345678901234567890" + $"{1e17:R}" |> equal "1E+17" + $"{0.1 + 0.2:R}" |> equal "0.30000000000000004" + let format = "R" + (1e-7).ToString(format, CultureInfo.InvariantCulture) |> equal "1E-07" + testCase "byte.ToString 'X' works" <| fun () -> (255uy).ToString("X2") |> equal "FF" (255uy).ToString("X15") |> equal "0000000000000FF" From a55511976b7c232cb9cdf8cfb70503e756684195 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 29 Oct 2026 20:07:51 +0100 Subject: [PATCH 276/322] chore: add citation metadata for Zenodo (#5220) --- .zenodo.json | 667 +++++++++++++++++++++++++++++++++++++++++++++++++++ CITATION.cff | 27 +++ 2 files changed, 694 insertions(+) create mode 100644 .zenodo.json create mode 100644 CITATION.cff diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000000..c46b6fa9a0 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,667 @@ +{ + "title": "Fable: F# to JavaScript, TypeScript, Python, Rust, Erlang and Dart compiler", + "description": "Fable compiles F# to JavaScript, TypeScript, Python, Rust, Dart and Erlang/BEAM. It is built on the F# Compiler Services and ships runtime libraries for each target, so F# code can run natively in those ecosystems.", + "license": "MIT", + "upload_type": "software", + "access_right": "open", + "keywords": [ + "F#", + "compiler", + "transpiler", + "JavaScript", + "TypeScript", + "Python", + "Rust", + "Dart", + "Erlang", + "BEAM" + ], + "creators": [ + { + "name": "García-Caro, Alfonso" + }, + { + "name": "Brattli, Dag" + }, + { + "name": "ncave" + }, + { + "name": "Mangel, Maxime" + } + ], + "contributors": [ + { + "name": "Forkmann, Steffen", + "type": "Other" + }, + { + "name": "Verdonck, Florian", + "type": "Other" + }, + { + "name": "Ajaj, Zaid", + "type": "Other" + }, + { + "name": "Swan, Alex", + "type": "Other" + }, + { + "name": "Podhola, David", + "type": "Other" + }, + { + "name": "Nosik, Ilja", + "type": "Other" + }, + { + "name": "Petricek, Tomas", + "type": "Other" + }, + { + "name": "fdcastel", + "type": "Other" + }, + { + "name": "Grund, Joe", + "type": "Other" + }, + { + "name": "Hietanen, Tuomas", + "type": "Other" + }, + { + "name": "Teasdale, David", + "type": "Other" + }, + { + "name": "kerams", + "type": "Other" + }, + { + "name": "Corrado, Alex", + "type": "Other" + }, + { + "name": "Claes, Jens", + "type": "Other" + }, + { + "name": "Cieślak, Krzysztof", + "type": "Other" + }, + { + "name": "Laïly, Melvyn", + "type": "Other" + }, + { + "name": "Booksbaum", + "type": "Other" + }, + { + "name": "Heintz, Aleksander", + "type": "Other" + }, + { + "name": "Jansson, Tomas", + "type": "Other" + }, + { + "name": "goswinr", + "type": "Other" + }, + { + "name": "Johnson, Cody", + "type": "Other" + }, + { + "name": "shayanhabibi", + "type": "Other" + }, + { + "name": "zach-interstates", + "type": "Other" + }, + { + "name": "Young, Zack", + "type": "Other" + }, + { + "name": "K, Mark", + "type": "Other" + }, + { + "name": "Zhao, Taine", + "type": "Other" + }, + { + "name": "Vitko, Valery", + "type": "Other" + }, + { + "name": "Dummann, Kolja", + "type": "Other" + }, + { + "name": "stroborobo", + "type": "Other" + }, + { + "name": "Shaw, Dave", + "type": "Other" + }, + { + "name": "Ohlrich, Jonathan", + "type": "Other" + }, + { + "name": "Trelford, Phillip", + "type": "Other" + }, + { + "name": "njlr", + "type": "Other" + }, + { + "name": "Chassaing, Jérémie", + "type": "Other" + }, + { + "name": "Thomas, Dave", + "type": "Other" + }, + { + "name": "Pauan", + "type": "Other" + }, + { + "name": "chadunit", + "type": "Other" + }, + { + "name": "Scites, Scott", + "type": "Other" + }, + { + "name": "Rivaille, PY", + "type": "Other" + }, + { + "name": "Clarke, Tom", + "type": "Other" + }, + { + "name": "Rieger, Lukas", + "type": "Other" + }, + { + "name": "Wostenberg, John", + "type": "Other" + }, + { + "name": "Soikin, Fyodor", + "type": "Other" + }, + { + "name": "Conforti, Leo", + "type": "Other" + }, + { + "name": "Thornton, Matt", + "type": "Other" + }, + { + "name": "piaste", + "type": "Other" + }, + { + "name": "Carter, Phillip", + "type": "Other" + }, + { + "name": "gusty", + "type": "Other" + }, + { + "name": "Brodeur, Francois", + "type": "Other" + }, + { + "name": "Esmerio, Diego", + "type": "Other" + }, + { + "name": "Dawkins, David", + "type": "Other" + }, + { + "name": "cannorin", + "type": "Other" + }, + { + "name": "i-p", + "type": "Other" + }, + { + "name": "Ott, Caroline", + "type": "Other" + }, + { + "name": "oopbase", + "type": "Other" + }, + { + "name": "Frèrebeau, Rémy", + "type": "Other" + }, + { + "name": "Leko, Tomas", + "type": "Other" + }, + { + "name": "McLellan, Michael", + "type": "Other" + }, + { + "name": "Harding, Eric", + "type": "Other" + }, + { + "name": "roboz0r", + "type": "Other" + }, + { + "name": "Sousa, Henrique", + "type": "Other" + }, + { + "name": "anchann", + "type": "Other" + }, + { + "name": "Smith, Stephen", + "type": "Other" + }, + { + "name": "Treanor, Simon", + "type": "Other" + }, + { + "name": "Hester, Jared", + "type": "Other" + }, + { + "name": "Sada, Enrico", + "type": "Other" + }, + { + "name": "Grebeniuk, Evgenii", + "type": "Other" + }, + { + "name": "Zafeiris, Orfeas", + "type": "Other" + }, + { + "name": "human-bean-555", + "type": "Other" + }, + { + "name": "Blair, Paul", + "type": "Other" + }, + { + "name": "Keše, Peter", + "type": "Other" + }, + { + "name": "Cullmann, Stefan", + "type": "Other" + }, + { + "name": "Sauleau, Sven", + "type": "Other" + }, + { + "name": "Bouquain, Thierry", + "type": "Other" + }, + { + "name": "do-wa", + "type": "Other" + }, + { + "name": "halfabench", + "type": "Other" + }, + { + "name": "irium", + "type": "Other" + }, + { + "name": "Verschatse, Gien", + "type": "Other" + }, + { + "name": "zanaptak", + "type": "Other" + }, + { + "name": "Segay, Gauthier", + "type": "Other" + }, + { + "name": "mizzle-mo", + "type": "Other" + }, + { + "name": "Rizzotto, Alessandro", + "type": "Other" + }, + { + "name": "Ågren, Andreas", + "type": "Other" + }, + { + "name": "Kurdiumov, Andrii", + "type": "Other" + }, + { + "name": "Hally, Bill", + "type": "Other" + }, + { + "name": "Churchill, Dave", + "type": "Other" + }, + { + "name": "Manske, Ian", + "type": "Other" + }, + { + "name": "Chang, Ji", + "type": "Other" + }, + { + "name": "Price, Joseph", + "type": "Other" + }, + { + "name": "Roncaglia, Julien", + "type": "Other" + }, + { + "name": "Tseing", + "type": "Other" + }, + { + "name": "Dittrich, Matthias", + "type": "Other" + }, + { + "name": "Malook, Max", + "type": "Other" + }, + { + "name": "Wilson, Max", + "type": "Other" + }, + { + "name": "Gumus, Onur", + "type": "Other" + }, + { + "name": "Frèrebeau, Rémy", + "type": "Other" + }, + { + "name": "Sim, Robert", + "type": "Other" + }, + { + "name": "ritcoder", + "type": "Other" + }, + { + "name": "Joux, Phil de", + "type": "Other" + }, + { + "name": "Pelech, Ondra", + "type": "Other" + }, + { + "name": "Forral, Oliver", + "type": "Other" + }, + { + "name": "Dunets, Nick", + "type": "Other" + }, + { + "name": "Darvey, Nick", + "type": "Other" + }, + { + "name": "Samudro, Naser Anjum", + "type": "Other" + }, + { + "name": "Janger, Mike", + "type": "Other" + }, + { + "name": "Stangarlin, Luiz Felipe", + "type": "Other" + }, + { + "name": "Luis, Leo Lorenzo", + "type": "Other" + }, + { + "name": "Battermann, Leif", + "type": "Other" + }, + { + "name": "Nischal, Kurren", + "type": "Other" + }, + { + "name": "Gerasimenko, Kirill", + "type": "Other" + }, + { + "name": "rbauduin", + "type": "Other" + }, + { + "name": "olivercoad", + "type": "Other" + }, + { + "name": "jonasdw", + "type": "Other" + }, + { + "name": "cr3wdayt5p", + "type": "Other" + }, + { + "name": "Podlovics, Zoltan", + "type": "Other" + }, + { + "name": "Shchur, Vladimir", + "type": "Other" + }, + { + "name": "Heimowski, Tomasz", + "type": "Other" + }, + { + "name": "Forkmann, Tim", + "type": "Other" + }, + { + "name": "Badger, The Gitter", + "type": "Other" + }, + { + "name": "Skar, Tengel", + "type": "Other" + }, + { + "name": "drk-mtr", + "type": "Other" + }, + { + "name": "TeaDrivenDev", + "type": "Other" + }, + { + "name": "Officer, Steve", + "type": "Other" + }, + { + "name": "Kirkvik, Simen L.", + "type": "Other" + }, + { + "name": "Tihon, Sergey", + "type": "Other" + }, + { + "name": "Melnikov, Roman", + "type": "Other" + }, + { + "name": "Boudereau, Clément", + "type": "Other" + }, + { + "name": "Queiróz, Adelar da Silva", + "type": "Other" + }, + { + "name": "Ball, Alan", + "type": "Other" + }, + { + "name": "Gorchakov, Artyom V.", + "type": "Other" + }, + { + "name": "bklop", + "type": "Other" + }, + { + "name": "Taylor, Ben", + "type": "Other" + }, + { + "name": "Taggart, Cameron", + "type": "Other" + }, + { + "name": "Meeren, Christer van der", + "type": "Other" + }, + { + "name": "Bouillier, Clément", + "type": "Other" + }, + { + "name": "McMinn, Damon", + "type": "Other" + }, + { + "name": "DashieTM", + "type": "Other" + }, + { + "name": "weebs", + "type": "Other" + }, + { + "name": "Programmerino", + "type": "Other" + }, + { + "name": "Gusarov, Eugene", + "type": "Other" + }, + { + "name": "Gabasova, Evelina", + "type": "Other" + }, + { + "name": "Romain, Flechner", + "type": "Other" + }, + { + "name": "fbehrens", + "type": "Other" + }, + { + "name": "Bilkus, Gary", + "type": "Other" + }, + { + "name": "Bouquain, Gaël", + "type": "Other" + }, + { + "name": "Tang, Hadrian", + "type": "Other" + }, + { + "name": "iyegoroff", + "type": "Other" + }, + { + "name": "Titkov, Ilya", + "type": "Other" + }, + { + "name": "Fajfr, Jan", + "type": "Other" + }, + { + "name": "Beekmann, Joerg", + "type": "Other" + }, + { + "name": "Egger, Johannes", + "type": "Other" + }, + { + "name": "Mols, Johannes", + "type": "Other" + }, + { + "name": "josselinauguste", + "type": "Other" + }, + { + "name": "Skripsky, Juraj", + "type": "Other" + }, + { + "name": "jstarkadf", + "type": "Other" + }, + { + "name": "Sacks, Justin", + "type": "Other" + }, + { + "name": "Frey, Kevin", + "type": "Other" + }, + { + "name": "Lambert, Kevin", + "type": "Other" + } + ] +} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..0efea4b0ae --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,27 @@ +cff-version: 1.2.0 +message: If you use Fable in your work, please cite it using this metadata. +type: software +title: "Fable: F# to JavaScript, TypeScript, Python, Rust, Erlang and Dart compiler" +abstract: Fable compiles F# to JavaScript, TypeScript, Python, Rust, Dart and Erlang/BEAM. It is built on the F# Compiler Services and ships runtime libraries for each target, so F# code can run natively in those ecosystems. +authors: + - family-names: García-Caro + given-names: Alfonso + - family-names: Brattli + given-names: Dag + - name: ncave + - family-names: Mangel + given-names: Maxime +repository-code: https://github.com/fable-compiler/Fable +url: https://fable.io +license: MIT +keywords: + - F# + - compiler + - transpiler + - JavaScript + - TypeScript + - Python + - Rust + - Dart + - Erlang + - BEAM From cac5ddd9a99205e0bb5272f1ffd8f0cca937824c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:08:10 +0100 Subject: [PATCH 277/322] chore: Bump the all-npm group with 2 updates (#5205) Signed-off-by: dependabot[bot] --- package-lock.json | 407 +++++++++++++++++++++++++--------------------- package.json | 4 +- 2 files changed, 227 insertions(+), 184 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31f85d180d..07780f8783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,12 @@ "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.9", + "rollup": "^4.65.0", "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { - "markdownlint-cli": "^0.49.1" + "markdownlint-cli": "^0.50.0" }, "engines": { "node": ">=26" @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.9.tgz", - "integrity": "sha512-qJmhckvOleXp8R+BaXiVW/S34/3M7pvN17/QcG90A74BDaSvKv8ffY+GYG+a/xU0vEoITzdMSiM7o9HZSQWbfA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.9.tgz", - "integrity": "sha512-dqQgds39BNWGB2ughJ6n2LTQFXk8RAOwnXj6m387OHpygfD93QRS1ODIfpiWyAa9gsFB9l34pSqeLmA0Xk/VBw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", "cpu": [ "arm64" ], @@ -114,9 +114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.9.tgz", - "integrity": "sha512-scrMx4SGikOSbWknLmPD0v6OeJ1Wih3kyRLdNZnlVbBeKVWp1nRTVcpM0IxDJdc3u8UjesPHTmnX649u8cQpBw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", "cpu": [ "arm64" ], @@ -127,9 +127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.9.tgz", - "integrity": "sha512-fkwi2OQR+QWoViRFt08Bsca6G0e9pk/4z+JgmqHq6jSRTT0LtI27KRVK8bu++6L/CBtQ3OZePbqC3bcrwGzTqQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", "cpu": [ "x64" ], @@ -140,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.9.tgz", - "integrity": "sha512-8Apr0Khn0r2MUsDtcOMbp85fDdXsX/E+iLhO17WUjG24m3O8ChArqI0isI7CAdj9Ouq177PHtVtEUpMNE1C4Gw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", "cpu": [ "arm64" ], @@ -153,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.9.tgz", - "integrity": "sha512-H+upy4DhBcBZlUsCAMyr8wR0Dg9kj8sGloVZdZ7va2heJM/5jnROxI47dtXseJjlD/VoQe6MrXhY3b5ikOq/Vw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", "cpu": [ "x64" ], @@ -166,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.9.tgz", - "integrity": "sha512-b50NS9TbAEK/b16vBoKrkeNwy+HvGBXmtBbzX4wcodWOh9dQZvRTN9rUyElIXa36QnwXhU9PAVVkZpqRzSi2Tg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", "cpu": [ "arm" ], @@ -182,9 +182,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.9.tgz", - "integrity": "sha512-SdqcudB/FzjLESsRMgXhV+FXGtf6usxXwlMoRW7D/XS3f9CZ4T4wycRBu2h9bVl2AOaPpunoA8ftAM/MF4altQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", "cpu": [ "arm" ], @@ -198,9 +198,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.9.tgz", - "integrity": "sha512-dg19FFRoovN+KZ+gMHBAq4Do8PgQmBcxpuhux57OuiR0QLavfn8uPBY8CP+UO50bO+xf0tK0GlqI/jld++5E6g==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", "cpu": [ "arm64" ], @@ -214,9 +214,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.9.tgz", - "integrity": "sha512-+jyv5ofGmpAvtrzLjSr/e4syskeikEp5L82bUzB76auwOgK1VNVwZV/F2g1Pes9bhTznbejOefAqPglHSKYmTA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", "cpu": [ "arm64" ], @@ -230,9 +230,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.9.tgz", - "integrity": "sha512-MKFXxqmkzCbtNvx/vjiJdss44eRtVabhCtDWcCFD9NHGwP+YkqnM/slnEsv/3fsoq9ukTtudraAjGhptrUPCVg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", "cpu": [ "loong64" ], @@ -246,9 +246,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.9.tgz", - "integrity": "sha512-lhARcOT6n7Ni9hpQRq8LZjbuoW+G/GXaW4Jfhx2MbmjnCj9XxLMRTceJIMpiMLQVWm+qxe7oq1sYvlIGOAW34Q==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", "cpu": [ "loong64" ], @@ -262,9 +262,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.9.tgz", - "integrity": "sha512-oORQzGxZ1oHghJwLdwc8Ni0SY9otILAeXsWdiImTF4F5HkWJUwh5s/269PF5tYILNC1z/3RwFsrgcHPguBcb0g==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", "cpu": [ "ppc64" ], @@ -278,9 +278,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.9.tgz", - "integrity": "sha512-J2JQHQt7oR1uutd2OSwbV2Qpb2LI5gggPolbkUwDe7vn/0dnduZ0w6EIbGnZUAw7uIKaMgfVylcH6V+Aov8kRA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", "cpu": [ "ppc64" ], @@ -294,9 +294,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.9.tgz", - "integrity": "sha512-bWaKLrfzeQ9mOzJJxXbPgw2RH6VAaTJrb/60Tqf7YSMxubG8780Hzm7Us+D6qVW8QRGJYl4VUvVXxnRB0o9zvQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", "cpu": [ "riscv64" ], @@ -310,9 +310,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.9.tgz", - "integrity": "sha512-TFJyaCB9NsvSyvVGQgTSRD9XgxsAq99ivDDkt9KAySlrNW5Obsd1kQpvoCvBCOLq+JlJTjmtroLH0wp8M8mEsA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", "cpu": [ "riscv64" ], @@ -326,9 +326,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.9.tgz", - "integrity": "sha512-DvmOM5xz/wDnUJFVjqP32ahalxn7BDuvO25bpuFOrb50AIFCCcFPN4lWWUDD1c8Ddv9sMO9SXyqtOet8G8u9cQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", "cpu": [ "s390x" ], @@ -342,9 +342,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.9.tgz", - "integrity": "sha512-aFzokHK3yE00mzAvKm/PY76OsOOTjvoTGE82Vp89iBDyD2wUBlV4ZsR2tWkRECTEFk6TC4Yc3zaEWjPqq1vstQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", "cpu": [ "x64" ], @@ -358,9 +358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.9.tgz", - "integrity": "sha512-t/yMdeaaQUuNNpmT7taBmH2VvHzgT4YA/IaKVE3qVr4PYQemn/kmJkL4uG4UvTLcWXQd0MSoOa/3URYSWGmUEg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.9.tgz", - "integrity": "sha512-4dnxKQ8GHARyjqAU87q5UWhywZTpIbR2VWhtACdC37GD1SGszmvb3TaBFVA4n+fnDHSKMut4ZJ63hVFwgnxVtg==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", "cpu": [ "x64" ], @@ -387,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.9.tgz", - "integrity": "sha512-1qdQj6IrNoR0xFvrCQHmhBXe9HMbBGNTFi+3n4jKMt2rTx4k0aeAdraZU4WDOMfL9tAwS8/TRSuquvNuPQ+Gpw==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", "cpu": [ "arm64" ], @@ -400,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.9.tgz", - "integrity": "sha512-yc1GSEHIYhKGN5WSaJc+eMy8bZvzcvAmflEgFh8L3xcy8dYxGmfrudyBnBFde9f6JzOa1kFqocF8BHaWybjQoA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", "cpu": [ "arm64" ], @@ -413,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.9.tgz", - "integrity": "sha512-VzHGvosAkLIzrwqvSgJBPDmGFTF/yE8OtMT4H5z8pM1NA0NiPpvTYnUh+9xPd+d5oy/wqF317FmJ1I8Kk8pp0Q==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", "cpu": [ "ia32" ], @@ -426,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.9.tgz", - "integrity": "sha512-FB77txy1PiAcJgG72nOLA6R0S3PRZahs8Spx0z2yseDUkoMmJcrQghiebJ2JHZ74cCOVlX/MYd4dUbJtJJ8fow==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", "cpu": [ "x64" ], @@ -439,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.9.tgz", - "integrity": "sha512-IoIgQLpK1DbOcQf1vX+i5oe5LXypsCVb1j/mU6BLAbOGmXH4E+j4kMfgZRwPYUohVBurNyVMgIKVM5dR6xA0kA==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", "cpu": [ "x64" ], @@ -873,11 +873,21 @@ } }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", "dev": true, - "license": "Python-2.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" }, "node_modules/balanced-match": { "version": "4.0.4", @@ -1178,13 +1188,13 @@ "license": "MIT" }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -1306,9 +1316,9 @@ } }, "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { @@ -1434,9 +1444,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz", - "integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", "dev": true, "funding": [ { @@ -1450,7 +1460,7 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "argparse": "^3.0.3" }, "bin": { "js-yaml": "bin/js-yaml.mjs" @@ -1519,9 +1529,9 @@ } }, "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", "dev": true, "funding": [ { @@ -1535,7 +1545,7 @@ ], "license": "MIT", "dependencies": { - "uc.micro": "^2.0.0" + "uc.micro": "^3.0.0" } }, "node_modules/lodash.uniq": { @@ -1545,9 +1555,9 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.3.3", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.3.tgz", - "integrity": "sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", "dev": true, "funding": [ { @@ -1561,33 +1571,34 @@ ], "license": "MIT", "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "uc.micro": "^3.0.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdownlint": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", - "integrity": "sha512-qHKeU2E1bdyNAT077go2FVTNXvYcktN5IHtF6XyeD1l0PClxzSp2tUApAV14ORI8DGX4H9bNKZEzelZp4qn8IA==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.42.0.tgz", + "integrity": "sha512-kzsCV/rPWRFn3jUhcB919YF4Qgeh0g9tcogoXFadGSo6Zxj6662YOqWXv0jDn8nv5KCCxDfMKJZv6N4HkNkvTg==", "dev": true, "license": "MIT", "dependencies": { - "micromark": "4.0.2", - "micromark-core-commonmark": "2.0.3", + "micromark": "4.0.3", + "micromark-core-commonmark": "2.0.4", "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.1", + "micromark-extension-gfm-table": "2.1.2", "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.2", - "string-width": "8.2.1" + "micromark-util-types": "2.0.3", + "re2js": "2.8.6", + "string-width": "8.3.0" }, "engines": { "node": ">=22" @@ -1597,23 +1608,23 @@ } }, "node_modules/markdownlint-cli": { - "version": "0.49.1", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.49.1.tgz", - "integrity": "sha512-qpYqJbSYf3jv57bdnFmCaZ/Wlu6IYHp2b6SOKrKBJ7OnPrDHIKmx4NERWH49QH9viTI6yO6raVDDn5nrf60VQQ==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.50.0.tgz", + "integrity": "sha512-4GqP/CqpF74ihrlLTOPc3I5b/tZabiiKnIVQ27xTzsS4hjU3oIHKg1YBzWP7fsa9AH48LPF0KcmCqDccpSeMEg==", "dev": true, "license": "MIT", "dependencies": { "commander": "~15.0.0", "deep-extend": "~0.6.0", - "ignore": "~7.0.6", - "js-yaml": "~5.2.1", + "ignore": "~7.0.13", + "js-yaml": "~5.5.0", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", - "markdown-it": "~14.3.0", - "markdownlint": "~0.41.1", - "minimatch": "~10.2.5", + "markdown-it": "~15.0.3", + "markdownlint": "~0.42.0", + "minimatch": "~10.2.6", "run-con": "~1.3.3", - "smol-toml": "~1.7.0", + "smol-toml": "~1.9.1", "tinyglobby": "~0.2.17" }, "bin": { @@ -1624,16 +1635,16 @@ } }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", "dev": true, "license": "MIT" }, "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", "dev": true, "funding": [ { @@ -1657,6 +1668,7 @@ "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", @@ -1667,9 +1679,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", "dev": true, "funding": [ { @@ -1687,18 +1699,19 @@ "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", + "micromark-factory-space": "^2.1.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.3" } }, "node_modules/micromark-extension-directive": { @@ -1760,9 +1773,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", "dev": true, "license": "MIT", "dependencies": { @@ -1843,9 +1856,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "dev": true, "funding": [ { @@ -2013,6 +2026,26 @@ "micromark-util-symbol": "^2.0.0" } }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -2150,9 +2183,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", "dev": true, "funding": [ { @@ -2167,12 +2200,12 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -2303,6 +2336,16 @@ "node": ">=6" } }, + "node_modules/re2js": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.6.tgz", + "integrity": "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2330,9 +2373,9 @@ } }, "node_modules/rollup": { - "version": "4.64.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.9.tgz", - "integrity": "sha512-mnPSQINChN7PpT+a5CI0/+87hm/+6zxSKBK1XonrzQ0zFnglaTjhkTP2Br4yNCTcLHUEvrAfZffH57jGf+8xBQ==", + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -2346,31 +2389,31 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.64.9", - "@rollup/rollup-android-arm64": "4.64.9", - "@rollup/rollup-darwin-arm64": "4.64.9", - "@rollup/rollup-darwin-x64": "4.64.9", - "@rollup/rollup-freebsd-arm64": "4.64.9", - "@rollup/rollup-freebsd-x64": "4.64.9", - "@rollup/rollup-linux-arm-gnueabihf": "4.64.9", - "@rollup/rollup-linux-arm-musleabihf": "4.64.9", - "@rollup/rollup-linux-arm64-gnu": "4.64.9", - "@rollup/rollup-linux-arm64-musl": "4.64.9", - "@rollup/rollup-linux-loong64-gnu": "4.64.9", - "@rollup/rollup-linux-loong64-musl": "4.64.9", - "@rollup/rollup-linux-ppc64-gnu": "4.64.9", - "@rollup/rollup-linux-ppc64-musl": "4.64.9", - "@rollup/rollup-linux-riscv64-gnu": "4.64.9", - "@rollup/rollup-linux-riscv64-musl": "4.64.9", - "@rollup/rollup-linux-s390x-gnu": "4.64.9", - "@rollup/rollup-linux-x64-gnu": "4.64.9", - "@rollup/rollup-linux-x64-musl": "4.64.9", - "@rollup/rollup-openbsd-x64": "4.64.9", - "@rollup/rollup-openharmony-arm64": "4.64.9", - "@rollup/rollup-win32-arm64-msvc": "4.64.9", - "@rollup/rollup-win32-ia32-msvc": "4.64.9", - "@rollup/rollup-win32-x64-gnu": "4.64.9", - "@rollup/rollup-win32-x64-msvc": "4.64.9", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", "fsevents": "~2.3.2" } }, @@ -2464,9 +2507,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.2.tgz", - "integrity": "sha512-pXFZ9B2WinEPzxWkMmlYE/oYx2BP+qLrE95wP8tCuK901uLSMGdCb6QSr82z+wnhXkG4+cO+OMLbZB2Cn+97zw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2511,9 +2554,9 @@ } }, "node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2738,9 +2781,9 @@ } }, "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index c5043aa79d..bc097d971c 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,11 @@ "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.64.9", + "rollup": "^4.65.0", "terser": "^5.51.2", "typescript": "^7.0.2" }, "devDependencies": { - "markdownlint-cli": "^0.49.1" + "markdownlint-cli": "^0.50.0" } } From 865d3c3eca917168ba03cdc16a23d4d97ece2fa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:27:12 +0100 Subject: [PATCH 278/322] chore: release multiple projects (#5221) --- src/Fable.Cli/CHANGELOG.md | 11 ++++++++++- src/Fable.Compiler/CHANGELOG.md | 11 ++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 ++-- src/fable-compiler-js/CHANGELOG.md | 11 ++++++++++- src/fable-compiler-js/package.json | 4 ++-- src/fable-library-ts/CHANGELOG.md | 11 ++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 11 ++++++++++- src/fable-standalone/package.json | 4 ++-- 9 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 9d6215b9a4..99077cc6ce 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 +last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.26.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + ## 5.25.0 - 2026-10-29 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 7fcc218130..077c24ac7d 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 +last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.25.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + ## 5.24.0 - 2026-10-29 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index 128caf6123..da346cc1af 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.25.0" + let VERSION = "5.26.0" [] - let JS_LIBRARY_VERSION = "2.11.0" + let JS_LIBRARY_VERSION = "2.12.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 46e9686e9c..2584f7a336 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 +last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.4.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + ## 4.3.0 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 698c05f08d..3847043490 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "4.3.0", + "version": "4.4.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.2.0", - "@fable-org/fable-standalone": "^4.0.0-beta.4" + "@fable-org/fable-standalone": "^4.0.0-beta.5" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 00c444f24a..7fb47cb333 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 +last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c updaters: - package.json: file: package.json @@ -15,6 +15,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.12.0 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + ## 2.11.0 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index 1685a2a430..bc92c97362 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.11.0", + "version": "2.12.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index 7760b1ebd2..f33587fd25 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: ce886964cb4521dc4c01c8da089b97c9d9742814 +last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c pre_release: beta include: - ../Fable.Transforms/ @@ -21,6 +21,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.5 - 2026-10-29 + +### 🚀 Features + +* *(js/ts)* Support the "R" numeric format and choose "G" notation like .NET (#5212) ([24d9c41a](https://github.com/fable-compiler/Fable/commit/24d9c41ad69993f0c4e843558133bcd787a960d4)) +* *(js/ts/python/dart/beam)* Support ArgumentException.ParamName (#5203) ([8ed77788](https://github.com/fable-compiler/Fable/commit/8ed7778893bab48c7dd41d7322d26d2bcaba6d99)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/ce886964cb4521dc4c01c8da089b97c9d9742814..cac5ddd9a99205e0bb5272f1ffd8f0cca937824c) + ## 4.0.0-beta.4 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index 36582fb96f..be2e5e066f 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "4.0.0-beta.4", - "fableVersion": "5.25.0", + "version": "4.0.0-beta.5", + "fableVersion": "5.26.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 7b176e7e13bee118a21b8358e6eed65c19ac9549 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 14:42:43 +0100 Subject: [PATCH 279/322] fix(js/ts): round float32 results like .NET (#5222) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 17 ++++++-- src/Fable.Transforms/Replacements.fs | 51 +++++++++++++++-------- src/fable-library-ts/Double.ts | 12 ++++++ tests/Js/Main/ArithmeticTests.fs | 24 +++++++++++ 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index cda3307bce..d37172ff2c 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -2096,7 +2096,7 @@ module Util = | Some(Assign left) -> ExpressionStatement(assign babelExpr.Location left babelExpr) | Some(Target left) -> ExpressionStatement(assign babelExpr.Location (left |> Expression.Identifier) babelExpr) - let transformOperation com ctx range opKind : Expression = + let transformOperation com ctx range typ opKind : Expression = match opKind with | Fable.Unary(op, TransformExpr com ctx expr) -> Expression.unaryExpression (op, expr, ?loc = range) @@ -2132,7 +2132,16 @@ module Util = Expression.binaryExpression (op, left, right, ?loc = range) | _, TransformExpr com ctx left, TransformExpr com ctx right -> - Expression.binaryExpression (op, left, right, ?loc = range) + let e = Expression.binaryExpression (op, left, right, ?loc = range) + + match op, typ with + // Rounding the float64 result of one operation to float32 equals the float32 operation + | (BinaryPlus | BinaryMinus | BinaryMultiply | BinaryDivide | BinaryModulus), Fable.Number(Float32, _) -> + let fround = + Expression.memberExpression (Expression.identifier "Math", Expression.identifier "fround") + + Expression.callExpression (fround, [| e |], ?loc = range) + | _ -> e | Fable.Logical(op, TransformExpr com ctx left, TransformExpr com ctx right) -> Expression.logicalExpression (left, op, right, ?loc = range) @@ -3288,7 +3297,7 @@ but thanks to the optimisation done below we get | Fable.CurriedApply(callee, args, _, range) -> transformCurriedApply com ctx range callee args - | Fable.Operation(kind, _, _, range) -> transformOperation com ctx range kind + | Fable.Operation(kind, _, t, range) -> transformOperation com ctx range t kind | Fable.Get(expr, kind, typ, range) -> transformGet com ctx range typ expr kind @@ -3420,7 +3429,7 @@ but thanks to the optimisation done below we get [| resolveExpr t returnStrategy e |] | Fable.Operation(kind, _, t, range) -> - [| transformOperation com ctx range kind |> resolveExpr t returnStrategy |] + [| transformOperation com ctx range t kind |> resolveExpr t returnStrategy |] | Fable.Get(expr, kind, t, range) -> [| transformGet com ctx range t expr kind |> resolveExpr t returnStrategy |] diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index b3128c53fc..232c798961 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -264,27 +264,38 @@ let needToCast fromKind toKind = let h = kindIndex toKind // return type (horizontal) ((v > h) || (v < 4 && h > 3)) && (h < 8) || (h <> v && (h = 11 || v = 11)) +let roundToFloat32 r t (expr: Expr) = + match t with + | Number(Float32, _) -> emitExpr r t [ expr ] "Math.fround($0)" + | _ -> expr + /// Conversions to floating point let toFloat com (ctx: Context) r targetType (args: Expr list) : Expr = - match args.Head.Type with - | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) - | String -> - let style = - int ( - System.Globalization.NumberStyles.Float - ||| System.Globalization.NumberStyles.AllowThousands - ) + let value = + match args.Head.Type with + | Char -> Helper.InstanceCall(args.Head, "charCodeAt", Int32.Number, [ makeIntConst 0 ]) + | String -> + let style = + int ( + System.Globalization.NumberStyles.Float + ||| System.Globalization.NumberStyles.AllowThousands + ) - Helper.LibCall(com, "Double", "parse", targetType, [ args.Head; makeIntConst style ]) - | Number(kind, _) -> - match kind with - | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) - | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args) - | _ -> TypeCast(args.Head, targetType) - | _ -> - addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" + Helper.LibCall(com, "Double", "parse", targetType, [ args.Head; makeIntConst style ]) + | Number(kind, _) -> + match kind with + | Decimal -> Helper.LibCall(com, "Decimal", "toFloat64", targetType, args) + | BigIntegers _ -> Helper.LibCall(com, "BigInt", "toFloat64", targetType, args) + | _ -> TypeCast(args.Head, targetType) + | _ -> + addWarning com ctx.InlinePath r "Cannot make conversion because source type is unknown" - TypeCast(args.Head, targetType) + TypeCast(args.Head, targetType) + + match args.Head.Type with + | Char + | Number((Float32 | Int8 | UInt8 | Int16 | UInt16), _) -> value + | _ -> roundToFloat32 r targetType value let toDecimal com (ctx: Context) r targetType (args: Expr list) : Expr = match args.Head.Type with @@ -1537,6 +1548,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Unlike Double.Parse, FSharp.Core's float and float32 remove underscores | ("ToSingle" | "ToDouble"), [ ExprType String as str ] -> Helper.LibCall(com, "Double", "parseIgnoringUnderscores", t, [ str ], ?loc = r) + |> roundToFloat32 r t |> Some | ("ToSingle" | "ToDouble"), _ -> toFloat com ctx r t args |> Some | "ToDecimal", _ -> toDecimal com ctx r t args |> Some @@ -2791,6 +2803,11 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op else [ str; style; makeBoolConst unsigned; makeIntConst bitsize ] @ outValue + let meth = + match kind with + | Float32 -> meth + "Single" + | _ -> meth + let callExpr = Helper.LibCall(com, numberModule, Naming.lowerFirst meth, t, args, ?loc = r) diff --git a/src/fable-library-ts/Double.ts b/src/fable-library-ts/Double.ts index ccda405933..fab6826826 100644 --- a/src/fable-library-ts/Double.ts +++ b/src/fable-library-ts/Double.ts @@ -35,6 +35,18 @@ export function parse(str: string, style: number = defaultStyle): number { } } +export function tryParseSingle(str: string, style: number, defValue: FSharpRef): boolean { + if (tryParse(str, style, defValue)) { + defValue.contents = Math.fround(defValue.contents); + return true; + } + return false; +} + +export function parseSingle(str: string, style?: number): number { + return Math.fround(parse(str, style)); +} + export function parseIgnoringUnderscores(str: string): number { return parse(str == null ? str : str.replace(/_/g, ""), NumberStyles.Float); } diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 240e9f6115..cd08db9a78 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -765,6 +765,30 @@ let tests = let result = Double.Parse("10.5", culture) result |> equal 10.5 + testCase "float32 arithmetic rounds every operation like .NET" <| fun () -> + let a, b, c = 1.5f, 1.2f, 3.3f + (a + b) * c |> equal 8.9099998f + a + b + c |> equal 6.f + 0.1f + 0.2f |> equal 0.3f + 16777216.f + 1.f |> equal 16777216.f + 1.f / 3.f |> equal 0.33333334f + 5.5f % 2.2f |> equal 1.0999999f + -(a + b) |> equal -2.7f + let arr = [| 0.f |] + arr.[0] <- a * c + arr.[0] |> equal 4.95f + + testCase "float32 conversions round like .NET" <| fun () -> + float32 2.7 |> equal 2.7f + float32 16777217 |> equal 16777216.f + float32 16777217L |> equal 16777216.f + float32 2.7M |> equal 2.7f + float32 "2.7" |> equal 2.7f + Single.Parse("2.7", CultureInfo.InvariantCulture) |> equal 2.7f + let mutable parsed = 0.f + Single.TryParse("2.7", NumberStyles.Float, CultureInfo.InvariantCulture, &parsed) |> equal true + parsed |> equal 2.7f + testCase "Single.Parse works with IFormatProvider" <| fun () -> // culture compiles to { } for now and it is ignored on the call-site let culture = Globalization.CultureInfo.InvariantCulture From 508026e73511f2636245515b8ec9476176e34b5c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 21:25:37 +0100 Subject: [PATCH 280/322] fix(js/ts/python): call base ctor from explicit constructors (#5226) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 30 ++++-- src/Fable.Transforms/FSharp2Fable.Util.fs | 67 +++++++++++++ src/Fable.Transforms/FSharp2Fable.fs | 36 ++++++- src/Fable.Transforms/FableTransforms.fs | 2 +- .../Python/Fable2Python.Transforms.fs | 40 ++++++-- tests/Js/Main/TypeTests.fs | 91 ++++++++++++++++++ tests/Python/TestType.fs | 96 +++++++++++++++++++ 7 files changed, 345 insertions(+), 17 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index d37172ff2c..ea587c2ea5 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -4448,6 +4448,18 @@ but thanks to the optimisation done below we get declareType com ctx ent entName doc args body baseExpr classMembers + let baseCallIdents (baseCall: Fable.Expr option) = + match baseCall with + | Some(Fable.Call(_, info, _, _)) -> + info.Args + |> List.choose ( + function + | Fable.IdentExpr id -> Some id + | _ -> None + ) + |> List.toArray + | _ -> [||] + let transformClassWithCompilerGeneratedConstructor (com: IBabelCompiler) ctx @@ -4459,19 +4471,25 @@ but thanks to the optimisation done below we get let fieldIds = getEntityFieldsAsIdents ent |> List.toArray let args = fieldIds |> Array.map identAsExpr - let baseExpr = + let baseExpr, superArgs, baseFieldIds = if ent.IsFSharpExceptionDeclaration then - libValue com ctx "Types" "FSharpException" |> SuperExpression |> Some + libValue com ctx "Types" "FSharpException" |> SuperExpression |> Some, [], [||] elif ent.IsFSharpRecord || ent.IsValueType then - libValue com ctx "Types" "Record" |> SuperExpression |> Some + libValue com ctx "Types" "Record" |> SuperExpression |> Some, [], [||] else - None + let baseType = + ent.BaseType + |> Option.map (fun d -> Fable.DeclaredType(d.Entity, d.GenericArgs)) + + match extractSuperClassFromBaseCall com ctx baseType decl.BaseCall with + | Some(baseExpr, superArgs) -> Some baseExpr, superArgs, baseCallIdents decl.BaseCall + | None -> None, [], [||] let body = BlockStatement( [| if Option.isSome baseExpr then - yield callSuperAsStatement [] + yield callSuperAsStatement superArgs yield! ent.FSharpFields |> List.filter (fun field -> not field.IsStatic) @@ -4487,7 +4505,7 @@ but thanks to the optimisation done below we get ) let args = - fieldIds + Array.append baseFieldIds fieldIds |> Array.map (fun fi -> Parameter.parameter (fi.Name, ?typeAnnotation = makeFieldAnnotationIfTypeScript com ctx fi.Type) ) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index f9f09bec8b..736d28489d 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -2421,6 +2421,52 @@ module Util = let getGenParamNames (types: Fable.Type list) : string list = getGenParams types |> List.map fst let getGenParamTypes (types: Fable.Type list) : Fable.Type list = getGenParams types |> List.map snd + let supportsCompilerGeneratedBaseCall (com: Compiler) = + match com.Options.Language with + | JavaScript + | TypeScript + | Python -> true + | _ -> false + + /// A class without a primary constructor gets a compiler-generated one that takes its fields + let hasCompilerGeneratedConstructor (ent: FSharpEntity) = + ent.IsClass + && not ent.IsFSharpExceptionDeclaration + && not ent.IsValueType + && not (isGlobalOrImportedFSharpEntity ent) + && (FsEnt.Ref ent).SourcePath.IsSome + && not (ent.MembersFunctionsAndValues |> Seq.exists _.IsImplicitConstructor) + + /// Base classes with compiler-generated constructors, root first, or None when a base has a primary constructor + let rec tryGetCompilerGeneratedBases (ent: FSharpEntity) : FSharpEntity list option = + match tryGetBaseEntity ent with + | None -> Some [] + | Some(baseEnt, _) when hasCompilerGeneratedConstructor baseEnt -> + tryGetCompilerGeneratedBases baseEnt + |> Option.map (fun bases -> bases @ [ baseEnt ]) + | Some _ -> None + + /// Inherited val fields root first, with the base generic arguments resolved through the chain + let getInheritedValFields (ent: FSharpEntity) (genArgs: Map) = + let rec collect (ent: FSharpEntity) genArgs acc = + match tryGetBaseEntity ent with + | Some(baseEnt, baseGenArgs) when hasCompilerGeneratedConstructor baseEnt -> + let baseGenArgs = + Seq.zip baseEnt.GenericParameters baseGenArgs + |> Seq.map (fun (param, typ) -> genParamName param, makeType genArgs typ) + |> Map.ofSeq + + let fields = + baseEnt.FSharpFields + |> Seq.filter (fun field -> not field.IsStatic) + |> Seq.map (fun field -> field, makeType baseGenArgs field.FieldType) + |> List.ofSeq + + collect baseEnt baseGenArgs (fields @ acc) + | _ -> acc + + collect ent genArgs [] + /// We can add a suffix to the entity name for special methods, like reflection declaration let entityIdentWithSuffix (com: Compiler) (ent: Fable.EntityRef) suffix = let error msg = @@ -2439,6 +2485,27 @@ module Util = let entityIdent (com: Compiler) (ent: Fable.EntityRef) = entityIdentWithSuffix com ent "" + /// Compiler-generated constructors of derived classes receive the inherited fields and pass them to the base + let tryMakeCompilerGeneratedBaseCall (com: Compiler) (ent: FSharpEntity) = + if supportsCompilerGeneratedBaseCall com && hasCompilerGeneratedConstructor ent then + match tryGetCompilerGeneratedBases ent with + | Some(_ :: _ as bases) -> + let baseEnt = List.last bases + + let args = + getInheritedValFields ent Map.empty + |> List.map (fun (field, typ) -> makeTypedIdentExpr typ field.Name) + + let baseExpr = + match tryGlobalOrImportedFSharpEntity com baseEnt with + | Some baseExpr -> baseExpr + | None -> FsEnt.Ref baseEnt |> entityIdent com + + Some(Fable.Call(baseExpr, Fable.CallInfo.Create(args = args), Fable.Unit, None)) + | _ -> None + else + None + /// In Python a union is emitted as a public type alias (`type Demo = Demo_A | Demo_B`) /// plus a private base class (`_Demo`) that holds the runtime members: attached static /// members and the `cases()` method used by reflection. The type alias is typing-only, diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 4153d5f4e2..46edbf930e 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -1578,6 +1578,38 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return makeCallFrom com ctx (makeRangeFrom fsExpr) typ genArgs None args memb + | FSharpExprPatterns.Sequential(ConstructorCall(baseCons, _, _) as first, + FSharpExprPatterns.NewRecord(fsType, ownArgs)) when + ctx.CaptureBaseConsCall.IsNone + && supportsCompilerGeneratedBaseCall com + && fsType.HasTypeDefinition + && ( + match baseCons.DeclaringEntity, tryGetBaseEntity fsType.TypeDefinition with + | Some callee, Some(baseEnt, _) -> callee = baseEnt + | _ -> false + ) + && (tryGetCompilerGeneratedBases fsType.TypeDefinition + |> Option.exists (List.isEmpty >> not)) + -> + let r = makeRangeFrom fsExpr + let! baseValue = transformExpr com ctx [] first + let! ownArgs = transformExprList com ctx ownArgs + + let baseIdent = getIdentUniqueName com ctx "base" |> makeTypedIdent baseValue.Type + + let inheritedArgs = + getInheritedValFields fsType.TypeDefinition ctx.GenericArgs + |> List.map (fun (field, typ) -> getFieldWith None typ (Fable.IdentExpr baseIdent) field.Name) + + let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) + let entRef = FsEnt.Ref fsType.TypeDefinition + entRef.SourcePath |> Option.iter com.AddSignatureDependency + + let newRecord = + Fable.NewRecord(inheritedArgs @ ownArgs, entRef, genArgs) |> makeValue r + + return Fable.Let(baseIdent, baseValue, newRecord) + | FSharpExprPatterns.Sequential(first, second) -> let exprs = match ctx.CaptureBaseConsCall with @@ -2418,7 +2450,7 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = Name = name Entity = entRef Constructor = None - BaseCall = None + BaseCall = tryMakeCompilerGeneratedBaseCall com fsEnt AttachedMembers = [] Tags = Fable.Tags.empty XmlDoc = tryGetXmlDoc fsEnt.XmlDoc @@ -3162,7 +3194,7 @@ let rec attachClassMembers (com: FableCompiler) = |> Option.map (fun members -> { decl with Constructor = members.Cons - BaseCall = members.BaseCall + BaseCall = members.BaseCall |> Option.orElse decl.BaseCall AttachedMembers = members.Members.ToArray() |> List.ofArray } |> Fable.ClassDeclaration diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 4742173eb6..907395faff 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -1020,7 +1020,7 @@ let rec transformDeclaration transformations (com: Compiler) file decl = let cons, baseCall = match decl.Constructor, decl.BaseCall with - | None, _ -> None, None + | None, baseCall -> None, baseCall | Some cons, None -> uncurryMemberArgs cons |> transformMemberBody com |> Some, None | Some cons, Some baseCall -> // In order to uncurry correctly the baseCall arguments, diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 6a626cb5b5..c77d94d28c 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -4498,6 +4498,7 @@ let transformClassWithCompilerGeneratedConstructor ctx (ent: Fable.Entity) (entName: string) + (baseCall: Fable.Expr option) classMembers (classAttributes: ClassAttributes) = @@ -4512,18 +4513,34 @@ let transformClassWithCompilerGeneratedConstructor let isOptional = Helpers.isOptional fieldIds || ent.IsFSharpRecord || ent.IsValueType - let baseExpr = + let baseExpr, superArgs, baseFieldIds, baseStmts = if ent.IsFSharpExceptionDeclaration then - libValue com ctx "exceptions" "FSharpException" |> Some + libValue com ctx "exceptions" "FSharpException" |> Some, [], [], [] elif ent.IsFSharpRecord || ent.IsValueType then - libValue com ctx "record" "Record" |> Some + libValue com ctx "record" "Record" |> Some, [], [], [] else - None + match extractBaseExprFromBaseCall com ctx ent.BaseType baseCall with + | Some(baseExpr, (superArgs, _keywords, stmts)) -> + let baseFieldIds = + match baseCall with + | Some(Fable.Call(_, info, _, _)) -> + info.Args + |> List.choose ( + function + | Fable.IdentExpr id -> Some id + | _ -> None + ) + | _ -> [] + + Some baseExpr, superArgs, baseFieldIds, stmts + | None -> None, [], [], [] let body = [ + yield! baseStmts + if Option.isSome baseExpr then - yield callSuperAsStatement [] + yield callSuperAsStatement superArgs // For attributes style, use direct attribute names and proper parameter handling if classAttributes.Style = ClassStyle.Attributes then @@ -4562,8 +4579,8 @@ let transformClassWithCompilerGeneratedConstructor ] let args = - fieldIds - |> Array.mapToList (fun id -> + baseFieldIds @ Array.toList fieldIds + |> List.map (fun id -> let ta, _ = Annotation.typeAnnotation com ctx None id.Type Arg.arg (ident com ctx id, annotation = ta) ) @@ -5388,7 +5405,14 @@ let rec transformDeclaration (com: IPythonCompiler) ctx (decl: Fable.Declaration allStatements @ classStmts | _, None -> let classStmts = - transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name allClassMembers classAttributes + transformClassWithCompilerGeneratedConstructor + com + ctx + ent + decl.Name + decl.BaseCall + allClassMembers + classAttributes allStatements @ classStmts diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index a1a73d7a25..76604f5c73 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -894,6 +894,60 @@ let walkLazyTree (tree: LazyTree>) : string = loop tree +type ValClassBase = + val Id: int + val Label: string + new (id: int) = { Id = id * 10; Label = "base" } + member this.Describe() = this.Label + ":" + string this.Id + +type ValClassChild = + inherit ValClassBase + val Extra: float + new (id: int, extra: float) = { inherit ValClassBase(id); Extra = extra } + +type ValClassGrandChild = + inherit ValClassChild + val Flag: bool + new (id: int, extra: float, flag: bool) = + let doubled = id * 2 + { inherit ValClassChild(doubled, extra); Flag = flag } + +let mutable valClassBaseCalls = 0 + +type ValClassCounting = + val N: int + new (n: int) = + valClassBaseCalls <- valClassBaseCalls + 1 + { N = n } + +type ValClassCountingChild = + inherit ValClassCounting + val M: int + new (n: int, m: int) = { inherit ValClassCounting(n); M = m } + +type ValClassDefaultBase = + val Tag: string + new () = { Tag = "default" } + +type ValClassDefaultChild = + inherit ValClassDefaultBase + val Value: int + new (value: int) = { inherit ValClassDefaultBase(); Value = value } + +type ValClassGenericBase<'T> = + val Value: 'T + new (v: 'T) = { Value = v } + +type ValClassIntBox = + inherit ValClassGenericBase + val Tag: string + new (v: int, t: string) = { inherit ValClassGenericBase(v); Tag = t } + +type ValClassPairBox<'U> = + inherit ValClassGenericBase<'U * 'U> + val Count: int + new (a: 'U, b: 'U) = { inherit ValClassGenericBase<'U * 'U>((a, b)); Count = 2 } + let tests = testList "Types" [ @@ -1838,4 +1892,41 @@ let tests = testCase "Nested lambda does not shadow an outer generic type parameter" <| fun () -> let box = mapGenericMethodBox 42 box.Get() |> List.ofSeq |> equal [ 42 ] + + testCase "Explicit constructor calls the base constructor" <| fun () -> + let child = ValClassChild(3, 1.5) + child.Id |> equal 30 + child.Label |> equal "base" + child.Extra |> equal 1.5 + child.Describe() |> equal "base:30" + (child :> ValClassBase).Id |> equal 30 + (box child :? ValClassBase) |> equal true + + testCase "Explicit constructors chain through several levels" <| fun () -> + let g = ValClassGrandChild(2, 0.5, true) + g.Id |> equal 40 + g.Label |> equal "base" + g.Extra |> equal 0.5 + g.Flag |> equal true + g.Describe() |> equal "base:40" + + testCase "Explicit constructor runs the base constructor once" <| fun () -> + valClassBaseCalls <- 0 + let c = ValClassCountingChild(5, 6) + valClassBaseCalls |> equal 1 + c.N |> equal 5 + c.M |> equal 6 + + testCase "Explicit constructor calls a parameterless base constructor" <| fun () -> + let c = ValClassDefaultChild(9) + c.Tag |> equal "default" + c.Value |> equal 9 + + testCase "Explicit constructor resolves the generic arguments of the base class" <| fun () -> + let intBox = ValClassIntBox(5, "x") + intBox.Value |> equal 5 + intBox.Tag |> equal "x" + let pairBox = ValClassPairBox("a", "b") + pairBox.Value |> equal ("a", "b") + pairBox.Count |> equal 2 ] diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index fc8d31ad32..cad1b1c9e3 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -1877,3 +1877,99 @@ let ``test Super call works correctly in multi-level generic class hierarchy`` ( obj.Attach("hello") // Each override delegates to base before logging, so the chain unwinds Base -> Mid -> Leaf log |> List.ofSeq |> equal [ "Base"; "Mid"; "Leaf" ] + +type ValClassBase = + val Id: int + val Label: string + new (id: int) = { Id = id * 10; Label = "base" } + member this.Describe() = this.Label + ":" + string this.Id + +type ValClassChild = + inherit ValClassBase + val Extra: float + new (id: int, extra: float) = { inherit ValClassBase(id); Extra = extra } + +type ValClassGrandChild = + inherit ValClassChild + val Flag: bool + new (id: int, extra: float, flag: bool) = + let doubled = id * 2 + { inherit ValClassChild(doubled, extra); Flag = flag } + +let mutable valClassBaseCalls = 0 + +type ValClassCounting = + val N: int + new (n: int) = + valClassBaseCalls <- valClassBaseCalls + 1 + { N = n } + +type ValClassCountingChild = + inherit ValClassCounting + val M: int + new (n: int, m: int) = { inherit ValClassCounting(n); M = m } + +type ValClassDefaultBase = + val Tag: string + new () = { Tag = "default" } + +type ValClassDefaultChild = + inherit ValClassDefaultBase + val Value: int + new (value: int) = { inherit ValClassDefaultBase(); Value = value } + +[] +let ``test explicit constructor calls the base constructor`` () = + let child = ValClassChild(3, 1.5) + child.Id |> equal 30 + child.Label |> equal "base" + child.Extra |> equal 1.5 + child.Describe() |> equal "base:30" + (child :> ValClassBase).Id |> equal 30 + (box child :? ValClassBase) |> equal true + +[] +let ``test explicit constructors chain through several levels`` () = + let g = ValClassGrandChild(2, 0.5, true) + g.Id |> equal 40 + g.Label |> equal "base" + g.Extra |> equal 0.5 + g.Flag |> equal true + g.Describe() |> equal "base:40" + +[] +let ``test explicit constructor runs the base constructor once`` () = + valClassBaseCalls <- 0 + let c = ValClassCountingChild(5, 6) + valClassBaseCalls |> equal 1 + c.N |> equal 5 + c.M |> equal 6 + +[] +let ``test explicit constructor calls a parameterless base constructor`` () = + let c = ValClassDefaultChild(9) + c.Tag |> equal "default" + c.Value |> equal 9 + +type ValClassGenericBase<'T> = + val Value: 'T + new (v: 'T) = { Value = v } + +type ValClassIntBox = + inherit ValClassGenericBase + val Tag: string + new (v: int, t: string) = { inherit ValClassGenericBase(v); Tag = t } + +type ValClassPairBox<'U> = + inherit ValClassGenericBase<'U * 'U> + val Count: int + new (a: 'U, b: 'U) = { inherit ValClassGenericBase<'U * 'U>((a, b)); Count = 2 } + +[] +let ``test explicit constructor resolves the generic arguments of the base class`` () = + let intBox = ValClassIntBox(5, "x") + intBox.Value |> equal 5 + intBox.Tag |> equal "x" + let pairBox = ValClassPairBox("a", "b") + pairBox.Value |> equal ("a", "b") + pairBox.Count |> equal 2 From e38a3022c601b2f5b9e76e79c1a0b4a2abb96ab5 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 22:18:23 +0100 Subject: [PATCH 281/322] test(ts/dart): cover generated generic name clash (#5228) --- tests/Dart/src/MiscTests.fs | 9 +++++++++ tests/TypeScript/LiteTsInteropTests.fs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/Dart/src/MiscTests.fs b/tests/Dart/src/MiscTests.fs index e532689428..ecee4fd97d 100644 --- a/tests/Dart/src/MiscTests.fs +++ b/tests/Dart/src/MiscTests.fs @@ -69,7 +69,16 @@ let paramName (f: unit -> unit) = "no exception" with :? System.ArgumentException as e -> e.ParamName +type GenericNameRecord<'a> = + { Value: 'a } + + static member Stringify v = string v + let tests() = + testCase "Compiler-generated generic names do not clash with the type's own" <| fun () -> + GenericNameRecord.Stringify 456 |> equal "456" + { Value = "x" }.Value |> equal "x" + testCase "ref works" <| fun () -> let x = ref 5 increase x diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index 5e70897578..49d2adbeca 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -57,8 +57,17 @@ module TypedArrays = let makeFloat32s () = JS.Constructors.Float32Array.Create [| 0.0; 1.0 |] #endif +type GenericNameRecord<'a> = + { Value: 'a } + + static member Stringify v = string v + let tests = testList "LiteTsInterop" [ + testCase "Compiler-generated generic names do not clash with the type's own" <| fun () -> + GenericNameRecord.Stringify 456 |> equal "456" + { Value = "x" }.Value |> equal "x" + #if FABLE_COMPILER testCase "jsOptions works" <| fun _ -> let opts = jsOptions(fun o -> From a782afcea79f91bc5649e7db5eea57f9d11a1529 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 30 Oct 2026 22:20:56 +0100 Subject: [PATCH 282/322] fix(all): recompile when a source file is added or its output is missing (#4870) Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Mangel Maxime --- src/Fable.Compiler/ProjectCracker.fs | 71 ++++++++++++ .../Integration/CacheInvalidationTests.fs | 102 ++++++++++++++++++ .../Fable.Tests.Integration.fsproj | 1 + tests/Integration/Integration/Main.fs | 1 + 4 files changed, 175 insertions(+) create mode 100644 tests/Integration/Integration/CacheInvalidationTests.fs diff --git a/src/Fable.Compiler/ProjectCracker.fs b/src/Fable.Compiler/ProjectCracker.fs index f381563ab6..69f3a6db1e 100644 --- a/src/Fable.Compiler/ProjectCracker.fs +++ b/src/Fable.Compiler/ProjectCracker.fs @@ -891,6 +891,73 @@ let loadPrecompiledInfo (opts: CrackerOptions) otherOptions sourceFiles = Some info, otherOptions, sourceFiles | None -> None, otherOptions, sourceFiles +/// Transitive `` targets plus the implicit Directory.Build.props/targets +/// and Directory.Packages.props found upwards. These can change the `` list +/// without touching any .fsproj timestamp, so the cache must invalidate on them too. +let private getProjectImports (projFile: string) : string list = + let imports = ResizeArray() + let visited = HashSet(StringComparer.OrdinalIgnoreCase) + + // Paths built from other properties are skipped below: missing an import costs + // a stale cache, guessing wrong costs a bogus path. + let expandMacros (fileDir: string) (path: string) = + let withSep = fileDir + string IO.Path.DirectorySeparatorChar + + path.Replace("$(MSBuildThisFileDirectory)", withSep).Replace("$(MSBuildProjectDirectory)", withSep) + + let rec collectFrom (file: string) = + // Guard against import cycles and repeated visits + if visited.Add(file) && IO.File.Exists(file) then + let fileDir = IO.Path.GetDirectoryName(file: string) + + let importPaths = + try + XDocument.Load(file).Descendants() + |> Seq.filter (fun el -> el.Name.LocalName = "Import") + |> Seq.choose (fun el -> + match el.Attribute(XName.Get "Project") with + | null -> None + | attr -> Some(expandMacros fileDir attr.Value) + ) + // Unresolved properties or wildcards need a real MSBuild evaluation + |> Seq.filter (fun path -> not (path.Contains("$(") || path.Contains("*"))) + |> Seq.toList + with _ -> + // A malformed or unreadable project is the cracker's problem, not ours + [] + + for importPath in importPaths do + let fullPath = + if IO.Path.IsPathRooted(importPath) then + importPath + else + IO.Path.Combine(fileDir, importPath) + + let fullPath = IO.Path.GetFullPath(fullPath) + + if IO.File.Exists(fullPath) then + imports.Add(fullPath) + collectFrom fullPath + + collectFrom (IO.Path.GetFullPath(projFile)) + + // Implicit imports the SDK adds for every project + let projDir = IO.Path.GetDirectoryName(IO.Path.GetFullPath(projFile)) + + for implicitFile in + [ + "Directory.Build.props" + "Directory.Build.targets" + "Directory.Packages.props" + ] do + match File.tryFindUpwards implicitFile projDir with + | Some path -> + imports.Add(path) + collectFrom path + | None -> () + + List.ofSeq imports + let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) : CrackerResponse = if not (IO.File.Exists(opts.ProjFile)) then Fable.FableError("Project file does not exist: " + opts.ProjFile) |> raise @@ -923,6 +990,10 @@ let getFullProjectOpts (resolver: ProjectCrackerResolver) (opts: CrackerOptions) && cacheInfo.ProjectPath = opts.ProjFile && cacheInfo.FableLib = opts.FableLib && cacheInfo.Replace = opts.Replace + // An imported .props/.targets can add or remove source files with no .fsproj touched + && ([ cacheInfo.ProjectPath; yield! cacheInfo.References ] + |> List.collect getProjectImports + |> List.forall isOlderThanCache) && ([ cacheInfo.ProjectPath; yield! cacheInfo.References ] |> List.forall (fun fsproj -> if IO.File.Exists(fsproj) && isOlderThanCache fsproj then diff --git a/tests/Integration/Integration/CacheInvalidationTests.fs b/tests/Integration/Integration/CacheInvalidationTests.fs new file mode 100644 index 0000000000..c19e7dfc8b --- /dev/null +++ b/tests/Integration/Integration/CacheInvalidationTests.fs @@ -0,0 +1,102 @@ +module Fable.Tests.CacheInvalidation + +open System +open System.IO +open Expecto + +/// A project whose compile order lives in an imported .props, so adding a source file +/// leaves the .fsproj timestamp untouched. In a temp dir: these tests mutate the project. +let private createProject (dir: string) = + Directory.CreateDirectory(dir) |> ignore + + File.WriteAllText( + Path.Combine(dir, "Test.fsproj"), + """ + + net10.0 + Major + + + +""" + ) + + File.WriteAllText( + Path.Combine(dir, "Tests.props"), + """ + + + + + +""" + ) + + File.WriteAllText(Path.Combine(dir, "Lib.fs"), "module Lib\n\nlet greeting = \"hello\"\n") + + File.WriteAllText(Path.Combine(dir, "Main.fs"), "module Main\n\nlet run () = printfn \"%s\" Lib.greeting\n") + +/// Cache invalidation compares timestamps, so a file rewritten within the same tick as the +/// previous compilation could look unchanged. Nudge it forward to keep the test from flaking. +let private writeNewer (path: string) (content: string) = + File.WriteAllText(path, content) + File.SetLastWriteTime(path, DateTime.Now.AddSeconds(2.0)) + +let private compile (dir: string) (outDir: string) = + Fable.Cli.Entry.main + [| + Path.Combine(dir, "Test.fsproj") + "--cwd" + dir + "--lang" + "javascript" + "--outDir" + outDir + |] + +let private withTempProject name (f: string -> string -> unit) = + let unique = Guid.NewGuid().ToString("N") + let dir = Path.Combine(Path.GetTempPath(), $"fable-cache-tests-%s{name}-%s{unique}") + + try + createProject dir + f dir (Path.Combine(dir, "out")) + finally + try + Directory.Delete(dir, true) + with _ -> + () + +let tests = + testList + "CacheInvalidation" + [ + // A `` arriving through an imported .props touches no .fsproj + // timestamp, so the options cache used to be reused with a stale source list. + testCase "Source file added via an imported .props is compiled" + <| fun () -> + withTempProject + "added" + (fun dir outDir -> + Expect.equal (compile dir outDir) 0 "First compilation should succeed" + + writeNewer (Path.Combine(dir, "Scalars.fs")) "module Scalars\n\nlet tests = \"scalars\"\n" + + writeNewer + (Path.Combine(dir, "Tests.props")) + """ + + + + + + +""" + + Expect.equal (compile dir outDir) 0 "Second compilation should succeed" + + Expect.isTrue + (File.Exists(Path.Combine(outDir, "Scalars.js"))) + "The file added through the imported .props should have been generated" + ) + ] diff --git a/tests/Integration/Integration/Fable.Tests.Integration.fsproj b/tests/Integration/Integration/Fable.Tests.Integration.fsproj index 56465bdd44..33b0ec33ad 100644 --- a/tests/Integration/Integration/Fable.Tests.Integration.fsproj +++ b/tests/Integration/Integration/Fable.Tests.Integration.fsproj @@ -17,6 +17,7 @@ + diff --git a/tests/Integration/Integration/Main.fs b/tests/Integration/Integration/Main.fs index 3346c5e75f..3ff92dce0a 100644 --- a/tests/Integration/Integration/Main.fs +++ b/tests/Integration/Integration/Main.fs @@ -6,6 +6,7 @@ let allTests = Cli.tests FileWatcher.tests CompilationTests.tests + CacheInvalidation.tests ] open Expecto From 3a3acf4ee213abb07fc4f7b98af69a6f8e68b571 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Fri, 30 Oct 2026 14:42:26 -0700 Subject: [PATCH 283/322] feat(js/ts/python/dart/rust/beam): support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) Co-authored-by: Mangel Maxime --- src/Fable.Transforms/Beam/Replacements.fs | 8 +++ src/Fable.Transforms/Dart/Replacements.fs | 7 ++ src/Fable.Transforms/Python/Replacements.fs | 7 ++ src/Fable.Transforms/Replacements.fs | 21 +++++- src/fable-library-beam/fable_convert.erl | 71 +++++++++++++++++++ src/fable-library-dart/String.dart | 40 +++++++++++ src/fable-library-py/fable_library/convert.py | 55 ++++++++++++++ src/fable-library-rust/src/Convert.rs | 3 + src/fable-library-ts/String.ts | 44 ++++++++++++ tests/Beam/EncodingTests.fs | 49 +++++++++++++ tests/Beam/Util.fs | 2 + tests/Dart/src/StringTests.fs | 40 +++++++++++ tests/Dart/src/Tests.Util.fs | 2 + tests/Js/Main/ConvertTests.fs | 56 +++++++++++---- tests/Python/TestConvert.fs | 64 ++++++++++++----- tests/Python/Util.fs | 2 + tests/Rust/tests/src/ConvertTests.fs | 5 ++ 17 files changed, 446 insertions(+), 30 deletions(-) create mode 100644 src/fable-library-py/fable_library/convert.py diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 11b28ecf45..69ce006d52 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -1768,6 +1768,14 @@ let private convert | "FromBase64String", [ arg ] -> Helper.LibCall(com, "fable_convert", "from_base64", t, [ arg ], ?loc = r) |> Some + | "ToHexString", ([ _ ] | [ _; _; _ ]) -> + Helper.LibCall(com, "fable_convert", "to_hex_string", t, args, ?loc = r) |> Some + | "ToHexStringLower", ([ _ ] | [ _; _; _ ]) -> + Helper.LibCall(com, "fable_convert", "to_hex_string_lower", t, args, ?loc = r) + |> Some + | "FromHexString", [ arg ] -> + Helper.LibCall(com, "fable_convert", "from_hex_string", t, [ arg ], ?loc = r) + |> Some | _ -> None /// Beam-specific List module replacements. diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index ce6705fd06..8e551f28a8 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -2828,6 +2828,13 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( ?loc = r ) |> Some + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 0f9ae88991..537e0f631d 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3026,6 +3026,13 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "Convert", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "Convert", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> Some | _ -> None let console (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 232c798961..f55b2b8233 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3432,6 +3432,13 @@ let bitConvert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option Helper.LibCall(com, "BitConverter", memberName, t, args, i.SignatureArgTypes, ?loc = r) |> Some +// The library decodes into a Uint8Array regardless of the typedArrays option +let private decodedBytes (com: ICompiler) r t expr = + if com.Options.TypedArrays then + expr + else + toArray r t expr + let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName with | "ToSByte" @@ -3456,7 +3463,19 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( if not (List.isSingle args) then WarningCodes.base64ArgumentsIgnored |> addWarningWithCode com ctx.InlinePath r - Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) + let expr = + Helper.LibCall(com, "String", (Naming.lowerFirst i.CompiledName), t, args, i.SignatureArgTypes, ?loc = r) + + match i.CompiledName with + | "FromBase64String" -> decodedBytes com r t expr |> Some + | _ -> Some expr + | "ToHexString" + | "ToHexStringLower" -> + Helper.LibCall(com, "String", Naming.lowerFirst i.CompiledName, t, args, i.SignatureArgTypes, ?loc = r) + |> Some + | "FromHexString" when List.isSingle args -> + Helper.LibCall(com, "String", "fromHexString", t, args, i.SignatureArgTypes, ?loc = r) + |> decodedBytes com r t |> Some | _ -> None diff --git a/src/fable-library-beam/fable_convert.erl b/src/fable-library-beam/fable_convert.erl index 8e5dd5627e..e2aeebef66 100644 --- a/src/fable-library-beam/fable_convert.erl +++ b/src/fable-library-beam/fable_convert.erl @@ -10,6 +10,11 @@ to_string_with_base/3, to_base64/1, from_base64/1, + to_hex_string/1, + to_hex_string/3, + to_hex_string_lower/1, + to_hex_string_lower/3, + from_hex_string/1, boolean_parse/1, boolean_try_parse/2, int_to_string_with_format/2, @@ -28,6 +33,11 @@ -spec to_string_with_base(integer(), integer(), integer()) -> binary(). -spec to_base64(tuple() | list() | binary()) -> binary(). -spec from_base64(binary()) -> tuple(). +-spec to_hex_string(tuple() | list() | binary() | reference() | undefined) -> binary(). +-spec to_hex_string(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary(). +-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined) -> binary(). +-spec to_hex_string_lower(tuple() | list() | binary() | reference() | undefined, integer(), integer()) -> binary(). +-spec from_hex_string(binary() | undefined) -> tuple(). -spec boolean_parse(binary()) -> boolean(). -spec boolean_try_parse(binary(), reference()) -> boolean(). -spec int_to_string_with_format(integer(), binary()) -> binary(). @@ -196,6 +206,67 @@ to_base64(Bin) when is_binary(Bin) -> from_base64(Str) when is_binary(Str) -> fable_utils:new_byte_array(binary_to_list(base64:decode(Str))). +to_hex_string(Bytes) -> + binary:encode_hex(hex_input(Bytes)). + +to_hex_string(Bytes, Offset, Length) -> + binary:encode_hex(hex_input(Bytes, Offset, Length)). + +%% binary:encode_hex/2 with lowercase needs OTP 26 +to_hex_string_lower(Bytes) -> + string:lowercase(to_hex_string(Bytes)). + +to_hex_string_lower(Bytes, Offset, Length) -> + string:lowercase(to_hex_string(Bytes, Offset, Length)). + +hex_input(undefined) -> + argument_null(<<"inArray">>); +hex_input({byte_array, _, _} = BA) -> + list_to_binary(fable_utils:byte_array_to_list(BA)); +hex_input(Bytes) when is_list(Bytes) -> + list_to_binary(Bytes); +hex_input(Bin) when is_binary(Bin) -> + Bin; +hex_input(Ref) when is_reference(Ref) -> + hex_input(get(Ref)). + +hex_input(Bytes, Offset, Length) -> + Bin = hex_input(Bytes), + if + Length < 0 -> argument_out_of_range(<<"length">>, <<"Non-negative number required.">>); + Offset < 0 -> argument_out_of_range(<<"offset">>, <<"Non-negative number required.">>); + Offset > byte_size(Bin) - Length -> + argument_out_of_range(<<"offset">>, <<"Specified argument was out of the range of valid values.">>); + true -> binary:part(Bin, Offset, Length) + end. + +from_hex_string(undefined) -> + argument_null(<<"s">>); +from_hex_string(Str) when byte_size(Str) rem 2 =/= 0 -> + bad_hex_string(<<"The input is not a valid hex string as its length is not a multiple of 2.">>); +from_hex_string(Str) -> + try binary:decode_hex(Str) of + Bin -> fable_utils:new_byte_array(binary_to_list(Bin)) + catch + error:badarg -> + bad_hex_string(<<"The input is not a valid hex string as it contains a non-hex character.">>) + end. + +argument_null(ParamName) -> + erlang:error(#{ + exn_type => argument_null_exception, + message => fable_utils:argument_exception_message(<<"Value cannot be null.">>, ParamName) + }). + +argument_out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). + +bad_hex_string(Message) -> + erlang:error(#{exn_type => format_exception, message => Message}). + %% TryParse: returns bool and sets out-param via put(OutRef, Value). %% F# uses out-parameter pattern: result = TryParse(str, &outRef). try_parse_int(Bin, OutRef) when is_binary(Bin) -> diff --git a/src/fable-library-dart/String.dart b/src/fable-library-dart/String.dart index 33b593c3c6..993afe2f3e 100644 --- a/src/fable-library-dart/String.dart +++ b/src/fable-library-dart/String.dart @@ -1,5 +1,7 @@ // ignore_for_file: file_names, constant_identifier_names +import 'System.dart' as system; + class StringComparison { static const CurrentCulture = 0; static const CurrentCultureIgnoreCase = 1; @@ -280,3 +282,41 @@ String mapIndexed(int Function(int, int) mapping, String str) { return mapping(index, rune); })); } + +String toHexStringLower(List inArray, [int? offset, int? length]) { + offset ??= 0; + length ??= inArray.length - offset; + if (length < 0) { + throw system.ArgumentOutOfRangeException_$ctor_Z384F8060( + 'length', 'Non-negative number required.'); + } + if (offset < 0) { + throw system.ArgumentOutOfRangeException_$ctor_Z384F8060( + 'offset', 'Non-negative number required.'); + } + if (offset > inArray.length - length) { + throw system.ArgumentOutOfRangeException_$ctor_Z721C83C5('offset'); + } + return inArray + .sublist(offset, offset + length) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); +} + +String toHexString(List inArray, [int? offset, int? length]) { + return toHexStringLower(inArray, offset, length).toUpperCase(); +} + +List fromHexString(String s) { + if (s.length % 2 != 0) { + throw system.FormatException_$ctor_Z721C83C5( + 'The input is not a valid hex string as its length is not a multiple of 2.'); + } + if (!RegExp(r'^[0-9A-Fa-f]*$').hasMatch(s)) { + throw system.FormatException_$ctor_Z721C83C5( + 'The input is not a valid hex string as it contains a non-hex character.'); + } + return List.generate( + s.length ~/ 2, (i) => int.parse(s.substring(2 * i, 2 * i + 2), radix: 16), + growable: false); +} diff --git a/src/fable-library-py/fable_library/convert.py b/src/fable-library-py/fable_library/convert.py new file mode 100644 index 0000000000..a95a600d6c --- /dev/null +++ b/src/fable-library-py/fable_library/convert.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import SupportsInt + +from .array_ import Array +from .core import byte +from .system import ( + ArgumentNullException__ctor_Z721C83C5, + ArgumentOutOfRangeException__ctor_Z384F8060, + ArgumentOutOfRangeException__ctor_Z721C83C5, + FormatException__ctor_Z721C83C5, +) + + +_HEX_DIGITS = frozenset("0123456789abcdefABCDEF") + + +def _hex_input(in_array: Array[byte], offset: SupportsInt | None, length: SupportsInt | None) -> bytes: + if in_array is None: + raise ArgumentNullException__ctor_Z721C83C5("inArray") + data = bytes(in_array) + start = 0 if offset is None else int(offset) + count = len(data) - start if length is None else int(length) + if count < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("length", "Non-negative number required.") + if start < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060("offset", "Non-negative number required.") + if start > len(data) - count: + raise ArgumentOutOfRangeException__ctor_Z721C83C5("offset") + return data[start : start + count] + + +def to_hex_string(in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None) -> str: + """Convert a byte array, or length bytes of it from offset, to uppercase hex (Convert.ToHexString).""" + return _hex_input(in_array, offset, length).hex().upper() + + +def to_hex_string_lower( + in_array: Array[byte], offset: SupportsInt | None = None, length: SupportsInt | None = None +) -> str: + """Convert a byte array, or length bytes of it from offset, to lowercase hex (Convert.ToHexStringLower).""" + return _hex_input(in_array, offset, length).hex() + + +def from_hex_string(s: str) -> Array[byte]: + """Convert a hex string of either case to a byte array (Convert.FromHexString).""" + if s is None: + raise ArgumentNullException__ctor_Z721C83C5("s") + if len(s) % 2 != 0: + raise FormatException__ctor_Z721C83C5( + "The input is not a valid hex string as its length is not a multiple of 2." + ) + if not _HEX_DIGITS.issuperset(s): + raise FormatException__ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character.") + return Array[byte](byte(x) for x in bytes.fromhex(s)) diff --git a/src/fable-library-rust/src/Convert.rs b/src/fable-library-rust/src/Convert.rs index 7aaf7b3e01..3531d764c5 100644 --- a/src/fable-library-rust/src/Convert.rs +++ b/src/fable-library-rust/src/Convert.rs @@ -268,6 +268,9 @@ pub mod Convert_ { } } let chars = s.as_bytes(); + if chars.len() % 2 != 0 { + panic!("The input is not a valid hex string as its length is not a multiple of 2."); + } let bytes = chars .chunks_exact(2) .map(|x| decode(x[0]) << 4 | decode(x[1])) diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index d12d60b696..15a5aa2c39 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -1,6 +1,7 @@ import { toString as dateToString } from "./Date.ts"; import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision, toRadix } from "./Numeric.ts"; import { escape } from "./RegExp.ts"; +import { ArgumentNullException_$ctor_Z721C83C5, ArgumentOutOfRangeException_$ctor_Z384F8060, ArgumentOutOfRangeException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; import { toString } from "./Types.ts"; import { Exception } from "./Util.ts"; @@ -683,6 +684,49 @@ export function fromBase64String(b64Encoded: string): number[] { return bytes as any as number[]; } +export function toHexString(inArray: ArrayLike, offset?: number, length?: number): string { + if (inArray == null) { + throw ArgumentNullException_$ctor_Z721C83C5("inArray"); + } + const start = offset ?? 0; + const count = length ?? inArray.length - start; + if (count < 0) { + throw ArgumentOutOfRangeException_$ctor_Z384F8060("length", "Non-negative number required."); + } + if (start < 0) { + throw ArgumentOutOfRangeException_$ctor_Z384F8060("offset", "Non-negative number required."); + } + if (start > inArray.length - count) { + throw ArgumentOutOfRangeException_$ctor_Z721C83C5("offset"); + } + let str = ""; + for (let i = start; i < start + count; i++) { + str += inArray[i].toString(16).padStart(2, "0"); + } + return str.toUpperCase(); +} + +export function toHexStringLower(inArray: ArrayLike, offset?: number, length?: number): string { + return toHexString(inArray, offset, length).toLowerCase(); +} + +export function fromHexString(s: string): number[] { + if (s == null) { + throw ArgumentNullException_$ctor_Z721C83C5("s"); + } + if (s.length % 2 !== 0) { + throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as its length is not a multiple of 2."); + } + if (!/^[0-9A-Fa-f]*$/.test(s)) { + throw FormatException_$ctor_Z721C83C5("The input is not a valid hex string as it contains a non-hex character."); + } + const bytes = new Uint8Array(s.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(s.substring(2 * i, 2 * i + 2), 16); + } + return bytes as any as number[]; +} + function pad(str: string, len: number, ch?: string, isRight?: boolean) { ch = ch || " "; len = len - str.length; diff --git a/tests/Beam/EncodingTests.fs b/tests/Beam/EncodingTests.fs index de1eaed3f0..5a54b9ed1e 100644 --- a/tests/Beam/EncodingTests.fs +++ b/tests/Beam/EncodingTests.fs @@ -192,6 +192,55 @@ let ``test Base64 empty array works`` () = let encoded = Convert.ToBase64String(bytes) encoded |> equal "" +// --- Hex --- + +[] +let ``test Convert.ToHexString works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + Convert.ToHexString(List.toArray [ 200uy; 3uy ]) |> equal "C803" + +[] +let ``test Convert.ToHexStringLower works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + +[] +let ``test Convert.ToHexString checks its arguments`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Convert.FromHexString works`` () = + Convert.FromHexString("fafBFCFDFE") |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + +[] +let ``test Convert.FromHexString checks its input`` () = + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" + // --- Guid --- [] diff --git a/tests/Beam/Util.fs b/tests/Beam/Util.fs index 28fd76e22c..8a8512c097 100644 --- a/tests/Beam/Util.fs +++ b/tests/Beam/Util.fs @@ -77,7 +77,9 @@ let raisedExceptionType (f: unit -> 'a): string = "no exception" with | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" | :? ArgumentException -> "ArgumentException" | :? InvalidOperationException -> "InvalidOperationException" | :? Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" + | :? FormatException -> "FormatException" | e -> "Unexpected exception: " + e.Message diff --git a/tests/Dart/src/StringTests.fs b/tests/Dart/src/StringTests.fs index 62f24fa4b7..ced331096a 100644 --- a/tests/Dart/src/StringTests.fs +++ b/tests/Dart/src/StringTests.fs @@ -1119,6 +1119,46 @@ let tests() = String.filter (fun x -> x <> '.') "" |> equal "" + testCase "Convert.ToHexString works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + + testCase "Convert.ToHexStringLower works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + + testCase "Convert.ToHexString checks its arguments" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "Convert.FromHexString works" <| fun () -> + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + + testCase "Convert.FromHexString checks its input" <| fun () -> + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" + // // TODO: Environment // #if FABLE_COMPILER // testCase "System.Environment.NewLine works" <| fun () -> diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index db6751fa4d..fad7e0d472 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -29,10 +29,12 @@ let raisedExceptionType (f: unit -> 'a): string = "no exception" with | :? System.ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? System.ArgumentNullException -> "ArgumentNullException" | :? System.ArgumentException -> "ArgumentException" | :? System.InvalidOperationException -> "InvalidOperationException" | :? System.Collections.Generic.KeyNotFoundException -> "KeyNotFoundException" | :? System.IndexOutOfRangeException -> "IndexOutOfRangeException" + | :? System.FormatException -> "FormatException" | e -> "Unexpected exception: " + e.Message let private run (f: unit -> 'a) = diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index bf7cb88013..b041395469 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -2241,19 +2241,49 @@ let tests = let x = "101" Convert.ToString(101.m) |> equal x - // testCase "Convert.ToHexString works" <| fun () -> - // let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] - // Convert.ToHexString(bytes) - // |> equal "FAFBFCFDFE" - - // testCase "Convert.ToHexStringLower works" <| fun () -> - // let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] - // Convert.ToHexStringLower(bytes) - // |> equal "fafbfcfdfe" - - // testCase "Convert.FromHexString works" <| fun () -> - // Convert.FromHexString("fafBFCFDFE") - // |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + testCase "Convert.ToHexString works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + + testCase "Convert.ToHexStringLower works" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + + testCase "Convert.ToHexString checks its arguments" <| fun () -> + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "Convert.FromHexString works" <| fun () -> + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + + testCase "Convert.FromHexString checks its input" <| fun () -> + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" testCase "Convert.ToBase64String works" <| fun () -> let bytes = [| 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy |] diff --git a/tests/Python/TestConvert.fs b/tests/Python/TestConvert.fs index a77a50fcaf..ea04995e4f 100644 --- a/tests/Python/TestConvert.fs +++ b/tests/Python/TestConvert.fs @@ -1060,22 +1060,54 @@ let ``test System.Convert.ToString Decimal works`` () = let x = "101" Convert.ToString(101.m) |> equal x -// [] -// let ``test Convert.ToHexString works`` () = -// let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] -// Convert.ToHexString(bytes) -// |> equal "FAFBFCFDFE" - -// [] -// let ``test Convert.ToHexStringLower works`` () = -// let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] -// Convert.ToHexStringLower(bytes) -// |> equal "fafbfcfdfe" - -// [] -// let ``test Convert.FromHexString works`` () = -// Convert.FromHexString("fafBFCFDFE") -// |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] +[] +let ``test Convert.ToHexString works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexString(bytes) + |> equal "FAFBFCFDFE" + Convert.ToHexString([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010F10" + Convert.ToHexString([||]) |> equal "" + Convert.ToHexString(bytes, 1, 3) |> equal "FBFCFD" + Convert.ToHexString(bytes, 5, 0) |> equal "" + +[] +let ``test Convert.ToHexStringLower works`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.ToHexStringLower(bytes) + |> equal "fafbfcfdfe" + Convert.ToHexStringLower([| 0uy; 1uy; 15uy; 16uy |]) |> equal "00010f10" + Convert.ToHexStringLower([||]) |> equal "" + Convert.ToHexStringLower(bytes, 1, 3) |> equal "fbfcfd" + +[] +let ``test Convert.ToHexString checks its arguments`` () = + let bytes = [| 250uy; 251uy; 252uy; 253uy; 254uy |] + raisedExceptionType (fun () -> Convert.ToHexString(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString((null: byte[]), 0, 0)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 1, -1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexString(bytes, 6, 0)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(null: byte[])) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, -1, 1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> Convert.ToHexStringLower(bytes, 3, 3)) |> equal "ArgumentOutOfRangeException" + +[] +let ``test Convert.FromHexString works`` () = + Convert.FromHexString("fafBFCFDFE") + |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] + Convert.FromHexString("0123456789abcdefABCDEF") + |> equal [| 1uy; 35uy; 69uy; 103uy; 137uy; 171uy; 205uy; 239uy; 171uy; 205uy; 239uy |] + Convert.FromHexString("") |> equal [||] + +[] +let ``test Convert.FromHexString checks its input`` () = + raisedExceptionType (fun () -> Convert.FromHexString(null: string)) |> equal "ArgumentNullException" + raisedExceptionType (fun () -> Convert.FromHexString("abc")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("zz")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("0x10")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("00 1")) |> equal "FormatException" + raisedExceptionType (fun () -> Convert.FromHexString("+1")) |> equal "FormatException" [] let ``test Convert.ToBase64String works`` () = diff --git a/tests/Python/Util.fs b/tests/Python/Util.fs index 72637b7aa4..9eeb542b5a 100644 --- a/tests/Python/Util.fs +++ b/tests/Python/Util.fs @@ -113,8 +113,10 @@ let raisedExceptionType (f: unit -> 'a): string = "no exception" with | :? ArgumentOutOfRangeException -> "ArgumentOutOfRangeException" + | :? ArgumentNullException -> "ArgumentNullException" | :? ArgumentException -> "ArgumentException" | :? InvalidOperationException -> "InvalidOperationException" + | :? FormatException -> "FormatException" | e -> "Unexpected exception: " + e.Message let Int32Array = [|1;2|] diff --git a/tests/Rust/tests/src/ConvertTests.fs b/tests/Rust/tests/src/ConvertTests.fs index cd71ebf032..2626b50b5d 100644 --- a/tests/Rust/tests/src/ConvertTests.fs +++ b/tests/Rust/tests/src/ConvertTests.fs @@ -1018,6 +1018,11 @@ let ``Convert.FromHexString works`` () = Convert.FromHexString("fafBFCFDFE") |> equal [| 250uy; 251uy; 252uy; 253uy; 254uy |] +[] +let ``Convert.FromHexString checks its input`` () = + throwsAnyError (fun () -> Convert.FromHexString("abc")) + throwsAnyError (fun () -> Convert.FromHexString("zz")) + [] let ``Convert.ToBase64String works`` () = let bytes = [| 2uy; 4uy; 6uy; 8uy; 10uy; 12uy; 14uy; 16uy; 18uy; 20uy |] From dd860cc179e0802d46eebb4110580e1edbf64cb8 Mon Sep 17 00:00:00 2001 From: Leo Conforti Date: Fri, 30 Oct 2026 14:53:49 -0700 Subject: [PATCH 284/322] fix(js/ts/python/dart/rust/beam): use FSharp.Core's algorithms for random collection functions (#5197) --- src/Fable.Transforms/Beam/Replacements.fs | 1 + src/Fable.Transforms/Dart/Replacements.fs | 1 + src/Fable.Transforms/Python/Replacements.fs | 15 +- src/Fable.Transforms/Replacements.fs | 1 + src/fable-library-beam/Seq.fs | 22 ++- src/fable-library-beam/fable_list.erl | 90 ++++++----- src/fable-library-beam/fable_random.erl | 40 ++++- src/fable-library-dart/Array.fs | 119 +++++++++------ src/fable-library-dart/List.fs | 9 +- src/fable-library-dart/Random.dart | 14 +- src/fable-library-dart/Seq.fs | 28 +++- src/fable-library-py/fable_library/List.fs | 3 + src/fable-library-py/fable_library/Seq.fs | 22 ++- src/fable-library-py/fable_library/random_.py | 30 ++++ src/fable-library-py/fable_library/util.py | 8 + src/fable-library-py/src/array.rs | 140 ++++++++++-------- src/fable-library-rust/src/Array.fs | 118 +++++++++------ src/fable-library-rust/src/List.fs | 9 +- src/fable-library-rust/src/Seq.fs | 28 +++- src/fable-library-ts/Array.fs | 114 ++++++++------ src/fable-library-ts/List.fs | 3 + src/fable-library-ts/Random.fs | 33 +++-- src/fable-library-ts/Seq.fs | 22 ++- tests/Beam/ArrayTests.fs | 35 +++++ tests/Beam/ListTests.fs | 6 + tests/Beam/RandomTests.fs | 17 +++ tests/Beam/SeqTests.fs | 14 ++ tests/Dart/src/ArrayTests.fs | 50 +++++++ tests/Dart/src/ListTests.fs | 10 ++ tests/Dart/src/RandomTests.fs | 14 ++ tests/Dart/src/SeqTests.fs | 17 +++ tests/Js/Main/ArrayTests.fs | 58 ++++++++ tests/Js/Main/ListTests.fs | 15 ++ tests/Js/Main/RandomTests.fs | 21 +++ tests/Js/Main/SeqTests.fs | 22 +++ tests/Python/TestArray.fs | 53 +++++++ tests/Python/TestList.fs | 7 + tests/Python/TestRandom.fs | 17 +++ tests/Python/TestSeq.fs | 14 ++ tests/Rust/tests/src/ArrayTests.fs | 53 +++++++ tests/Rust/tests/src/ListTests.fs | 12 ++ tests/Rust/tests/src/SeqTests.fs | 19 +++ 42 files changed, 1019 insertions(+), 305 deletions(-) create mode 100644 src/fable-library-py/fable_library/random_.py diff --git a/src/Fable.Transforms/Beam/Replacements.fs b/src/Fable.Transforms/Beam/Replacements.fs index 69ce006d52..429ef885da 100644 --- a/src/Fable.Transforms/Beam/Replacements.fs +++ b/src/Fable.Transforms/Beam/Replacements.fs @@ -4641,6 +4641,7 @@ let private randoms match args with | [ seed ] -> Helper.LibCall(com, "fable_random", "new_seeded", t, [ seed ], ?loc = r) |> Some | _ -> Helper.LibCall(com, "fable_random", "new", t, [], ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "fable_random", "shared", t, [], ?loc = r) |> Some | "Next", Some thisArg -> match args with | [] -> Helper.LibCall(com, "fable_random", "next", t, [ thisArg ], ?loc = r) |> Some diff --git a/src/Fable.Transforms/Dart/Replacements.fs b/src/Fable.Transforms/Dart/Replacements.fs index 8e551f28a8..01dcf7b65f 100644 --- a/src/Fable.Transforms/Dart/Replacements.fs +++ b/src/Fable.Transforms/Dart/Replacements.fs @@ -3297,6 +3297,7 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti ?loc = r ) |> Some + | "get_Shared", None -> Helper.LibCall(com, "Random", "shared", t, [], ?loc = r) |> Some // Not yet supported | ("NextInt64" | "NextSingle"), _ -> None | meth, Some thisArg -> diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index 537e0f631d..b83489bc1f 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -3481,17 +3481,18 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | ".ctor", _ -> Helper.LibCall(com, "util", "create_random", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "util", "random_shared", t, [], ?loc = r) |> Some | "Next", Some thisArg -> - let min, max = + let moduleName, meth, args = match args with - | [] -> makeIntConst 0, makeIntConst System.Int32.MaxValue - | [ max ] -> makeIntConst 0, max - | [ min; max ] -> min, max + | [] -> "util", "random_int", [ thisArg; makeIntConst 0; makeIntConst System.Int32.MaxValue ] + // These check the arguments like .NET + | [ _ ] -> "random_", "next_max", thisArg :: args + | [ _; _ ] -> "random_", "next_range", thisArg :: args | _ -> FableError "Unexpected arg count for Random.Next" |> raise - let args = [ thisArg; min; max ] - let argTypes = [ thisArg.Type; min.Type; max.Type ] - Helper.LibCall(com, "util", "random_int", t, args, argTypes, ?loc = r) |> Some + let argTypes = args |> List.map (fun a -> a.Type) + Helper.LibCall(com, moduleName, meth, t, args, argTypes, ?loc = r) |> Some | "NextDouble", Some thisArg -> let args = [ thisArg ] let argTypes = [ thisArg.Type ] diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index f55b2b8233..b80a88f6e4 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -4102,6 +4102,7 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti | args -> Helper.LibCall(com, "Random", "seeded", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "get_Shared", None -> Helper.LibCall(com, "Random", "shared", t, [], ?loc = r) |> Some // Not yet supported | ("NextInt64" | "NextSingle"), _ -> None | meth, Some thisArg -> diff --git a/src/fable-library-beam/Seq.fs b/src/fable-library-beam/Seq.fs index d4e496146e..cfc77eec02 100644 --- a/src/fable-library-beam/Seq.fs +++ b/src/fable-library-beam/Seq.fs @@ -1418,16 +1418,30 @@ let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs diff --git a/src/fable-library-beam/fable_list.erl b/src/fable-library-beam/fable_list.erl index 1cc2b2c97e..fefea00a62 100644 --- a/src/fable-library-beam/fable_list.erl +++ b/src/fable-library-beam/fable_list.erl @@ -779,7 +779,7 @@ take(Count, List) -> -spec random_shuffle_by(fun((ok) -> float()), list()) -> list(). random_shuffle_by(Randomizer, Xs) -> - shuffle_loop(Randomizer, erlang:list_to_tuple(Xs), erlang:length(Xs) - 1). + shuffle_loop(Randomizer, erlang:list_to_tuple(Xs), erlang:length(Xs), 0). -spec random_shuffle_with(reference(), list()) -> list(). random_shuffle_with(Random, Xs) -> @@ -793,14 +793,7 @@ random_shuffle(Xs) -> random_choice_by(_Randomizer, []) -> invalid_arg(<<"source">>, <<"The input sequence was empty.">>); random_choice_by(Randomizer, Xs) -> - Len = erlang:length(Xs), - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - randomizer_out_of_range(R); - true -> - lists:nth(erlang:trunc(R * Len) + 1, Xs) - end. + lists:nth(random_next(Randomizer, 0, erlang:length(Xs)) + 1, Xs). -spec random_choice_with(reference(), list()) -> term(). random_choice_with(Random, Xs) -> @@ -814,7 +807,7 @@ random_choice(Xs) -> random_choices_by(Randomizer, Count, Xs) -> if Count < 0 -> - invalid_arg(<<"count">>, <<"The input must be non-negative.">>); + negative_count(Count); Count > 0 andalso Xs =:= [] -> invalid_arg(<<"source">>, <<"The input sequence was empty.">>); true -> @@ -827,14 +820,8 @@ random_choices_by(Randomizer, Count, Xs) -> choices_loop(_Randomizer, _Arr, _Len, 0, Acc) -> lists:reverse(Acc); choices_loop(Randomizer, Arr, Len, N, Acc) -> - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - randomizer_out_of_range(R); - true -> - E = erlang:element(erlang:trunc(R * Len) + 1, Arr), - choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]) - end. + E = erlang:element(random_next(Randomizer, 0, Len) + 1, Arr), + choices_loop(Randomizer, Arr, Len, N - 1, [E | Acc]). -spec random_choices_with(reference(), non_neg_integer(), list()) -> list(). random_choices_with(Random, Count, Xs) -> @@ -848,7 +835,7 @@ random_choices(Count, Xs) -> random_sample_by(Randomizer, Count, Xs) -> if Count < 0 -> - invalid_arg(<<"count">>, <<"The input must be non-negative.">>); + negative_count(Count); true -> Len = erlang:length(Xs), if @@ -857,7 +844,11 @@ random_sample_by(Randomizer, Count, Xs) -> Count > Len -> invalid_arg(<<"count">>, <<"The input sequence has an insufficient number of elements.">>); true -> - sample_loop(Randomizer, erlang:list_to_tuple(Xs), Len, Count, 0) + Arr = erlang:list_to_tuple(Xs), + case Len =< max_set_size(Count) of + true -> sample_pool(Randomizer, Arr, Len, Count, 0, []); + false -> sample_set(Randomizer, Arr, Len, Count, #{}, []) + end end end. @@ -869,38 +860,55 @@ random_sample_with(Random, Count, Xs) -> random_sample(Count, Xs) -> random_sample_with(fable_random:new(), Count, Xs). -%% Partial Fisher-Yates: after Count swaps the first Count positions hold the sample. -sample_loop(_Randomizer, Arr, _Len, Count, I) when I >= Count -> - lists:sublist(erlang:tuple_to_list(Arr), Count); -sample_loop(Randomizer, Arr, Len, Count, I) -> - R = Randomizer(ok), - if - R < 0.0; R >= 1.0 -> - randomizer_out_of_range(R); +%% FSharp.Core's sampling: a shrinking pool for small inputs, otherwise redraw seen indices. +max_set_size(Count) when Count > 5 -> + 21 + erlang:trunc(math:pow(4, math:ceil(math:log(Count * 3) / math:log(4)))); +max_set_size(_Count) -> + 21. + +sample_pool(_Randomizer, _Pool, _Len, Count, I, Acc) when I >= Count -> + lists:reverse(Acc); +sample_pool(Randomizer, Pool, Len, Count, I, Acc) -> + J = random_next(Randomizer, 0, Len - I), + Item = erlang:element(J + 1, Pool), + Pool2 = erlang:setelement(J + 1, Pool, erlang:element(Len - I, Pool)), + sample_pool(Randomizer, Pool2, Len, Count, I + 1, [Item | Acc]). + +sample_set(_Randomizer, _Arr, _Len, 0, _Selected, Acc) -> + lists:reverse(Acc); +sample_set(Randomizer, Arr, Len, N, Selected, Acc) -> + J = random_next(Randomizer, 0, Len), + case maps:is_key(J, Selected) of true -> - J = I + erlang:trunc(R * (Len - I)), - EI = erlang:element(I + 1, Arr), - EJ = erlang:element(J + 1, Arr), - Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), - sample_loop(Randomizer, Arr2, Len, Count, I + 1) + sample_set(Randomizer, Arr, Len, N, Selected, Acc); + false -> + Item = erlang:element(J + 1, Arr), + sample_set(Randomizer, Arr, Len, N - 1, Selected#{J => true}, [Item | Acc]) end. -%% Full Fisher-Yates shuffle: builds result from index Len-1 downto 1. -shuffle_loop(_Randomizer, Arr, I) when I =< 0 -> +%% FSharp.Core's forward Fisher-Yates shuffle. +shuffle_loop(_Randomizer, Arr, Len, I) when I >= Len - 1 -> erlang:tuple_to_list(Arr); -shuffle_loop(Randomizer, Arr, I) -> +shuffle_loop(Randomizer, Arr, Len, I) -> + J = random_next(Randomizer, I, Len), + EI = erlang:element(I + 1, Arr), + EJ = erlang:element(J + 1, Arr), + Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), + shuffle_loop(Randomizer, Arr2, Len, I + 1). + +%% Like FSharp.Core's Random.next: an index in [Min, Max) from a randomizer result in [0.0, 1.0). +random_next(Randomizer, Min, Max) -> R = Randomizer(ok), if R < 0.0; R >= 1.0 -> randomizer_out_of_range(R); true -> - J = erlang:trunc(R * (I + 1)), - EI = erlang:element(I + 1, Arr), - EJ = erlang:element(J + 1, Arr), - Arr2 = erlang:setelement(I + 1, erlang:setelement(J + 1, Arr, EI), EJ), - shuffle_loop(Randomizer, Arr2, I - 1) + erlang:trunc(R * (Max - Min)) + Min end. +negative_count(Count) -> + invalid_arg(<<"count">>, <<"The input must be non-negative.\ncount = ", (erlang:integer_to_binary(Count))/binary>>). + randomizer_out_of_range(R) -> erlang:error(#{ exn_type => argument_out_of_range_exception, diff --git a/src/fable-library-beam/fable_random.erl b/src/fable-library-beam/fable_random.erl index dd6e1be1e0..92390ed102 100644 --- a/src/fable-library-beam/fable_random.erl +++ b/src/fable-library-beam/fable_random.erl @@ -3,6 +3,7 @@ -export([ new/0, new_seeded/1, + shared/0, next/1, next/2, next/3, @@ -27,6 +28,18 @@ new_seeded(Seed) -> put(Ref, rand:seed_s(exsss, Seed)), Ref. +%% Random.Shared: one unseeded instance per process. +-spec shared() -> reference(). +shared() -> + case get(fable_random_shared) of + undefined -> + Ref = new(), + put(fable_random_shared, Ref), + Ref; + Ref -> + Ref + end. + %% Random.Next() — returns a non-negative integer less than Int32.MaxValue. -spec next(reference()) -> non_neg_integer(). next(Ref) -> @@ -36,24 +49,29 @@ next(Ref) -> %% Random.Next(maxValue) — returns a non-negative integer less than maxValue. -spec next(reference(), integer()) -> non_neg_integer(). +next(_Ref, MaxValue) when MaxValue < 0 -> + out_of_range( + <<"maxValue">>, + <<"maxValue ('", (erlang:integer_to_binary(MaxValue))/binary, "') must be a non-negative value.">> + ); next(_Ref, 0) -> 0; -next(Ref, MaxValue) when MaxValue > 0 -> +next(Ref, MaxValue) -> {Val, NewState} = rand:uniform_s(MaxValue, get(Ref)), put(Ref, NewState), - Val - 1; -next(_, _) -> - erlang:error(badarg). + Val - 1. %% Random.Next(minValue, maxValue) — returns an integer in [minValue, maxValue). -spec next(reference(), integer(), integer()) -> integer(). -next(Ref, MinValue, MaxValue) when MaxValue > MinValue -> +next(_Ref, MinValue, MaxValue) when MinValue > MaxValue -> + out_of_range(<<"minValue">>, <<"'minValue' cannot be greater than maxValue.">>); +next(_Ref, MinValue, MinValue) -> + MinValue; +next(Ref, MinValue, MaxValue) -> Range = MaxValue - MinValue, {Val, NewState} = rand:uniform_s(Range, get(Ref)), put(Ref, NewState), - MinValue + Val - 1; -next(_, _, _) -> - erlang:error(badarg). + MinValue + Val - 1. %% Random.NextDouble() — returns a float in [0.0, 1.0). -spec next_double(reference()) -> float(). @@ -76,3 +94,9 @@ fill_random_bytes(Ref, AtomicsRef, Remaining, Idx) -> put(Ref, NewState), atomics:put(AtomicsRef, Idx, Val - 1), fill_random_bytes(Ref, AtomicsRef, Remaining - 1, Idx + 1). + +out_of_range(ParamName, Message) -> + erlang:error(#{ + exn_type => argument_out_of_range_exception, + message => fable_utils:argument_exception_message(Message, ParamName) + }). diff --git a/src/fable-library-dart/Array.fs b/src/fable-library-dart/Array.fs index 1753bb1914..50cf4e305f 100644 --- a/src/fable-library-dart/Array.fs +++ b/src/fable-library-dart/Array.fs @@ -1203,22 +1203,40 @@ let updateAt (index: int) (y: 'T) (xs: 'T[]) : 'T[] = xs[i] ) -let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = +// Like FSharp.Core's Random.next: rejects results outside [0.0, 1.0), NaN included +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + raise (System.ArgumentOutOfRangeException("randomizer", msg)) + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1229,21 +1247,17 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if xs.Length = 0 then - invalidArg "source" SR.inputSequenceEmpty + invalidArg "source" "The input array was empty." - let len = xs.Length - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) - - xs[int (r * float len)] + xs[randomNext randomizer 0 xs.Length] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1251,24 +1265,13 @@ let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = let randomChoice (xs: 'T[]) : 'T = randomChoiceWith (System.Random()) xs let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && xs.Length = 0 then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) - - xs[int (r * float len)] - ) + initialize count (fun _ -> xs[randomNext randomizer 0 len]) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1276,12 +1279,22 @@ let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = let randomChoices (count: int) (xs: 'T[]) : 'T[] = randomChoicesWith (System.Random()) count xs -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +// FSharp.Core's 21 + 4 ** ceil (log4 (3 * count)), kept to integers +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + let mutable size = 1 - let arr = copy xs - let len = arr.Length + while size < count * 3 do + size <- size * 4 + + 21 + size + else + 21 + +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1289,21 +1302,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) + if len <= getMaxSetSizeForSampling count then + let pool = copy xs + + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = HashSet() - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + for i = 0 to count - 1 do + let mutable j = next 0 len - getSubArray arr 0 count + while not (selected.Add j) do + j <- next 0 len + + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-dart/List.fs b/src/fable-library-dart/List.fs index 6d2d365faa..aa719947d7 100644 --- a/src/fable-library-dart/List.fs +++ b/src/fable-library-dart/List.fs @@ -1011,11 +1011,16 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T list) : 'T list = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = @@ -1036,7 +1041,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T list) : 'T l toArray xs |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T list) : 'T list = - randomSampleBy (fun () -> random.NextDouble()) count xs + toArray xs |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T list) : 'T list = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-dart/Random.dart b/src/fable-library-dart/Random.dart index d3b47305ad..af92afdef7 100644 --- a/src/fable-library-dart/Random.dart +++ b/src/fable-library-dart/Random.dart @@ -1,8 +1,6 @@ // ignore_for_file: file_names import 'dart:math' as dart_math; - -Exception _outOfRange(String paramName) => - Exception('Specified argument was out of the range of valid values. (Parameter $paramName)'); +import 'System.dart' as system; Exception _nullArg(String paramName) => Exception('Value cannot be null. (Parameter $paramName)'); @@ -16,14 +14,14 @@ class Random { int Next1(int maxValue) { if (maxValue < 0) { - throw _outOfRange('maxValue'); + throw system.ArgumentOutOfRangeException('maxValue', "maxValue ('$maxValue') must be a non-negative value."); } return Next2(0, maxValue); } int Next2(int minValue, int maxValue) { if (maxValue < minValue) { - throw _outOfRange('minValue'); + throw system.ArgumentOutOfRangeException('minValue', "'minValue' cannot be greater than maxValue."); } if (maxValue == minValue) { @@ -45,4 +43,8 @@ class Random { buffer[i] = _random.nextInt(256); } } -} \ No newline at end of file +} + +final Random _shared = Random(); + +Random shared() => _shared; diff --git a/src/fable-library-dart/Seq.fs b/src/fable-library-dart/Seq.fs index 6e63f866fe..92628a774f 100644 --- a/src/fable-library-dart/Seq.fs +++ b/src/fable-library-dart/Seq.fs @@ -1355,20 +1355,36 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1380,7 +1396,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T se xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = - randomSampleBy (fun () -> random.NextDouble()) count xs + xs |> toArray |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T seq) : 'T seq = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-py/fable_library/List.fs b/src/fable-library-py/fable_library/List.fs index c4d4f49048..d1ba04453e 100644 --- a/src/fable-library-py/fable_library/List.fs +++ b/src/fable-library-py/fable_library/List.fs @@ -1041,6 +1041,9 @@ let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = diff --git a/src/fable-library-py/fable_library/Seq.fs b/src/fable-library-py/fable_library/Seq.fs index 4456f22e7e..f6dbc827d8 100644 --- a/src/fable-library-py/fable_library/Seq.fs +++ b/src/fable-library-py/fable_library/Seq.fs @@ -1278,16 +1278,30 @@ let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs diff --git a/src/fable-library-py/fable_library/random_.py b/src/fable-library-py/fable_library/random_.py new file mode 100644 index 0000000000..b0c82a691a --- /dev/null +++ b/src/fable-library-py/fable_library/random_.py @@ -0,0 +1,30 @@ +"""System.Random methods that check their arguments like .NET (util.py cannot import the exception types).""" + +from __future__ import annotations + +import random + +from .system import ArgumentOutOfRangeException__ctor_Z384F8060 +from .util import random_int + + +def next_max(rand: random.Random, max_value: int) -> int: + """Random.Next(maxValue).""" + if max_value < 0: + raise ArgumentOutOfRangeException__ctor_Z384F8060( + "maxValue", f"maxValue ('{max_value}') must be a non-negative value." + ) + return random_int(rand, 0, max_value) + + +def next_range(rand: random.Random, min_value: int, max_value: int) -> int: + """Random.Next(minValue, maxValue).""" + if min_value > max_value: + raise ArgumentOutOfRangeException__ctor_Z384F8060("minValue", "'minValue' cannot be greater than maxValue.") + return random_int(rand, min_value, max_value) + + +__all__ = [ + "next_max", + "next_range", +] diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index e4aaf03d4c..7bb0f065eb 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -755,6 +755,13 @@ def create_random(seed: int | None = None) -> random.Random: return random.Random(None if seed is None else int(seed)) +_shared_random = random.Random() + + +def random_shared() -> random.Random: + return _shared_random + + def random_int(rand: random.Random, a: int, b: int) -> int: if a == b: return int32(a) @@ -980,6 +987,7 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "random_bytes", "random_double", "random_int", + "random_shared", "range", "round", "to_iterable", diff --git a/src/fable-library-py/src/array.rs b/src/fable-library-py/src/array.rs index 8d68d631f3..5a43882be3 100644 --- a/src/fable-library-py/src/array.rs +++ b/src/fable-library-py/src/array.rs @@ -3380,16 +3380,11 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, ) -> PyResult<()> { let len = self.storage.len(); - for i in (1..len).rev() { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); + for i in 0..len.saturating_sub(1) { + let j = random_next(randomizer, i, len)?; + if j != i { + self.swap_items(i, j); } - let j = (r * (i + 1) as f64) as usize; - self.swap_items(i, j); } Ok(()) } @@ -3397,10 +3392,9 @@ impl FSharpArray { pub fn random_shuffle_in_place_with( &mut self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult<()> { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_shuffle_in_place_by(py, &randomizer) } @@ -3423,10 +3417,9 @@ impl FSharpArray { pub fn random_shuffle_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_shuffle_by(py, &randomizer) } @@ -3444,26 +3437,18 @@ impl FSharpArray { let len = self.storage.len(); if len == 0 { return Err(PyErr::new::( - "The input sequence was empty.", + "The input array was empty.", )); } - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); - } - self.storage.get(py, (r * len as f64) as usize) + self.storage.get(py, random_next(randomizer, 0, len)?) } pub fn random_choice_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, ) -> PyResult> { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_choice_by(py, &randomizer) } @@ -3479,11 +3464,7 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - if count < 0 { - return Err(PyErr::new::( - "The input must be non-negative.", - )); - } + check_random_count(count)?; let count = count as usize; let xs_len = self.storage.len(); if count > 0 && xs_len == 0 { @@ -3501,14 +3482,7 @@ impl FSharpArray { let first = self.storage.get(py, 0)?; let mut result = FSharpArray::create(py, count, first.bind(py))?; for i in 0..count { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); - } - let src_idx = (r * xs_len as f64) as usize; + let src_idx = random_next(randomizer, 0, xs_len)?; let val = self.storage.get(py, src_idx)?; result.__setitem__(i as isize, val.bind(py), py)?; } @@ -3518,11 +3492,10 @@ impl FSharpArray { pub fn random_choices_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_choices_by(py, &randomizer, count) } @@ -3538,14 +3511,9 @@ impl FSharpArray { randomizer: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - if count < 0 { - return Err(PyErr::new::( - "The input must be non-negative.", - )); - } + check_random_count(count)?; let count = count as usize; - let mut arr = self.clone(); - let length = arr.storage.len(); + let length = self.storage.len(); if length == 0 && count > 0 { return Err(PyErr::new::( "The input sequence was empty.", @@ -3556,28 +3524,40 @@ impl FSharpArray { "The input sequence has an insufficient number of elements.", )); } - for i in 0..count { - let r: f64 = randomizer.call0()?.extract()?; - if r < 0.0 || r >= 1.0 { - return Err(PyErr::new::(format!( - "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", - r - ))); + if length == 0 { + return FSharpArray::empty(py, None); + } + // FSharp.Core's algorithm: a shrinking pool for small inputs, otherwise redraw seen indices + let mut result = self.get_sub_array(py, 0, count, None)?; + if length <= max_set_size_for_sampling(count) { + let mut pool: Vec = (0..length).collect(); + for i in 0..count { + let j = random_next(randomizer, 0, length - i)?; + let item = self.storage.get(py, pool[j])?; + result.__setitem__(i as isize, item.bind(py), py)?; + pool[j] = pool[length - i - 1]; + } + } else { + let mut selected = std::collections::HashSet::new(); + for i in 0..count { + let mut j = random_next(randomizer, 0, length)?; + while !selected.insert(j) { + j = random_next(randomizer, 0, length)?; + } + let item = self.storage.get(py, j)?; + result.__setitem__(i as isize, item.bind(py), py)?; } - let j = i + (r * (length - i) as f64) as usize; - arr.swap_items(i, j); } - arr.get_sub_array(py, 0, count, None) + Ok(result) } pub fn random_sample_with( &self, py: Python<'_>, - _random: &Bound<'_, PyAny>, + random: &Bound<'_, PyAny>, count: isize, ) -> PyResult { - let random_module = py.import("random")?; - let randomizer = random_module.getattr("random")?; + let randomizer = random.getattr("random")?; self.random_sample_by(py, &randomizer, count) } @@ -3588,6 +3568,40 @@ impl FSharpArray { } } +// FSharp.Core's Random.next, which rejects a randomizer result outside [0.0, 1.0), NaN included +fn random_next( + randomizer: &Bound<'_, PyAny>, + min_value: usize, + max_value: usize, +) -> PyResult { + let r: f64 = randomizer.call0()?.extract()?; + if !(0.0..1.0).contains(&r) { + return Err(PyErr::new::(format!( + "The index is outside the legal range.\nrandomizer returned {}, should be in range [0.0, 1.0).", + r + ))); + } + Ok((r * (max_value - min_value) as f64) as usize + min_value) +} + +fn check_random_count(count: isize) -> PyResult<()> { + if count < 0 { + return Err(PyErr::new::(format!( + "The input must be non-negative.\ncount = {}", + count + ))); + } + Ok(()) +} + +fn max_set_size_for_sampling(count: usize) -> usize { + if count > 5 { + 21 + 4f64.powf((count as f64 * 3.0).log(4.0).ceil()) as usize + } else { + 21 + } +} + // Loose functions that delegate to member functions #[pyfunction] #[pyo3(signature = (array1, array2, cons=None))] diff --git a/src/fable-library-rust/src/Array.fs b/src/fable-library-rust/src/Array.fs index 13c8f44d5b..b75ac417dd 100644 --- a/src/fable-library-rust/src/Array.fs +++ b/src/fable-library-rust/src/Array.fs @@ -985,22 +985,40 @@ let distinctBy<'T, 'Key when 'Key: equality and 'Key: not null> (projection: 'T let hashSet = System.Collections.Generic.HashSet<'Key>() xs |> filter (fun x -> hashSet.Add(projection x)) -let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = +// Like FSharp.Core's Random.next: rejects results outside [0.0, 1.0), NaN included +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + invalidArg "randomizer" msg + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1011,21 +1029,18 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if isEmpty xs then - invalidArg "source" SR.inputSequenceEmpty + invalidArg "source" "The input array was empty." let len = xs.Length - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] + xs[randomNext randomizer 0 len] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1033,24 +1048,13 @@ let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = let randomChoice (xs: 'T[]) : 'T = randomChoiceWith (System.Random()) xs let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && isEmpty xs then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException - - xs[int (r * float len)] - ) + initialize count (fun _ -> xs[randomNext randomizer 0 len]) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1058,12 +1062,22 @@ let randomChoicesWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = let randomChoices (count: int) (xs: 'T[]) : 'T[] = randomChoicesWith (System.Random()) count xs -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +// FSharp.Core's 21 + 4 ** ceil (log4 (3 * count)), kept to integers +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + let mutable size = 1 - let arr = copy xs - let len = arr.Length + while size < count * 3 do + size <- size * 4 + + 21 + size + else + 21 + +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1071,21 +1085,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count - if r < 0.0 || r >= 1.0 then - invalidArg "randomizer" SR.Arg_ArgumentOutOfRangeException + if len <= getMaxSetSizeForSampling count then + let pool = copy xs + + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = System.Collections.Generic.HashSet() - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + for i = 0 to count - 1 do + let mutable j = next 0 len - getSubArray arr 0 count + while not (selected.Add j) do + j <- next 0 len + + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-rust/src/List.fs b/src/fable-library-rust/src/List.fs index 4ce0353ec0..16fc523498 100644 --- a/src/fable-library-rust/src/List.fs +++ b/src/fable-library-rust/src/List.fs @@ -1009,11 +1009,16 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T list) : 'T list = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = @@ -1034,7 +1039,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T list) : 'T l toArray xs |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T list) : 'T list = - randomSampleBy (fun () -> random.NextDouble()) count xs + toArray xs |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T list) : 'T list = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-rust/src/Seq.fs b/src/fable-library-rust/src/Seq.fs index 76c9fbddce..eb818d6c63 100644 --- a/src/fable-library-rust/src/Seq.fs +++ b/src/fable-library-rust/src/Seq.fs @@ -1328,20 +1328,36 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T seq) : 'T seq = ofArray arr let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = toArray xs + Array.randomShuffleInPlaceWith random arr + ofArray arr let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs @@ -1353,7 +1369,7 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T se xs |> toArray |> Array.randomSampleBy randomizer count |> ofArray let randomSampleWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = - randomSampleBy (fun () -> random.NextDouble()) count xs + xs |> toArray |> Array.randomSampleWith random count |> ofArray let randomSample (count: int) (xs: 'T seq) : 'T seq = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-ts/Array.fs b/src/fable-library-ts/Array.fs index d1e0ee51e8..4b1b36c79d 100644 --- a/src/fable-library-ts/Array.fs +++ b/src/fable-library-ts/Array.fs @@ -1279,22 +1279,40 @@ let insertManyAt (index: int) (ys: seq<'T>) (xs: 'T[]) ([ float) (xs: 'T[]) : unit = +// Ports FSharp.Core's Random helpers so results and draw counts match .NET for the same seed +let private randomNext (randomizer: unit -> float) (minValue: int) (maxValue: int) : int = + let r = randomizer () + + if not (r >= 0.0 && r < 1.0) then + let msg = + "The index is outside the legal range.\nrandomizer returned " + + string r + + ", should be in range [0.0, 1.0)." + + raise (System.ArgumentOutOfRangeException("randomizer", msg)) + + int (r * float (maxValue - minValue)) + minValue + +let private checkRandomCount (count: int) = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + +let private shuffleInPlace (next: int -> int -> int) (xs: 'T[]) : unit = let len = xs.Length - for i = len - 1 downto 1 do - let r = randomizer () + for i = 0 to len - 2 do + let j = next i len - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) + if j <> i then + let tmp = xs[i] + xs[i] <- xs[j] + xs[j] <- tmp - let j = int (r * float (i + 1)) - let tmp = xs[i] - xs[i] <- xs[j] - xs[j] <- tmp +let randomShuffleInPlaceBy (randomizer: unit -> float) (xs: 'T[]) : unit = + shuffleInPlace (randomNext randomizer) xs let randomShuffleInPlaceWith (random: System.Random) (xs: 'T[]) : unit = - randomShuffleInPlaceBy (fun () -> random.NextDouble()) xs + shuffleInPlace (fun i n -> random.Next(i, n)) xs let randomShuffleInPlace (xs: 'T[]) : unit = randomShuffleInPlaceWith (System.Random()) xs @@ -1305,21 +1323,17 @@ let randomShuffleBy (randomizer: unit -> float) (xs: 'T[]) : 'T[] = arr let randomShuffleWith (random: System.Random) (xs: 'T[]) : 'T[] = - randomShuffleBy (fun () -> random.NextDouble()) xs + let arr = copy xs + randomShuffleInPlaceWith random arr + arr let randomShuffle (xs: 'T[]) : 'T[] = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T[]) : 'T = if isEmpty xs then - invalidArg "source" SR.inputSequenceEmpty - - let len = xs.Length - let r = randomizer () + invalidArg "source" "The input array was empty." - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) - - xs[int (r * float len)] + xs[randomNext randomizer 0 xs.Length] let randomChoiceWith (random: System.Random) (xs: 'T[]) : 'T = randomChoiceBy (fun () -> random.NextDouble()) xs @@ -1333,25 +1347,13 @@ let randomChoicesBy ([] cons: Cons<'T>) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative + checkRandomCount count if count > 0 && isEmpty xs then invalidArg "source" SR.inputSequenceEmpty let len = xs.Length - - initialize - count - (fun _ -> - let r = randomizer () - - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) - - xs[int (r * float len)] - ) - cons + initialize count (fun _ -> xs[randomNext randomizer 0 len]) cons let randomChoicesWith (random: System.Random) @@ -1365,12 +1367,16 @@ let randomChoicesWith let randomChoices (count: int) (xs: 'T[]) ([] cons: Cons<'T>) : 'T[] = randomChoicesWith (System.Random()) count xs cons -let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = - if count < 0 then - invalidArg "count" SR.inputMustBeNonNegative +let private getMaxSetSizeForSampling (count: int) = + if count > 5 then + 21 + int (4.0 ** ceil (System.Math.Log(float (count * 3), 4.0))) + else + 21 - let arr = copy xs - let len = arr.Length +// Picks from a shrinking pool for small inputs, otherwise draws indices until unseen (FSharp.Core, after CPython) +let private randomSampleWithNext (next: int -> int -> int) (count: int) (xs: 'T[]) : 'T[] = + checkRandomCount count + let len = xs.Length if len = 0 && count > 0 then invalidArg "source" SR.inputSequenceEmpty @@ -1378,21 +1384,33 @@ let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = if count > len then invalidArg "count" SR.notEnoughElements - for i = 0 to count - 1 do - let r = randomizer () + let result = getSubArray xs 0 count - if r < 0.0 || r >= 1.0 then - raise (System.ArgumentOutOfRangeException("randomizer", "The index is outside the legal range.")) + if len <= getMaxSetSizeForSampling count then + let pool = copy xs - let j = i + int (r * float (len - i)) - let tmp = arr[i] - arr[i] <- arr[j] - arr[j] <- tmp + for i = 0 to count - 1 do + let j = next 0 (len - i) + result[i] <- pool[j] + pool[j] <- pool[len - i - 1] + else + let selected = HashSet() + + for i = 0 to count - 1 do + let mutable j = next 0 len + + while not (selected.Add j) do + j <- next 0 len - getSubArray arr 0 count + result[i] <- xs[j] + + result + +let randomSampleBy (randomizer: unit -> float) (count: int) (xs: 'T[]) : 'T[] = + randomSampleWithNext (randomNext randomizer) count xs let randomSampleWith (random: System.Random) (count: int) (xs: 'T[]) : 'T[] = - randomSampleBy (fun () -> random.NextDouble()) count xs + randomSampleWithNext (fun a b -> random.Next(a, b)) count xs let randomSample (count: int) (xs: 'T[]) : 'T[] = randomSampleWith (System.Random()) count xs diff --git a/src/fable-library-ts/List.fs b/src/fable-library-ts/List.fs index 3d5b14dc1f..bb653c7c16 100644 --- a/src/fable-library-ts/List.fs +++ b/src/fable-library-ts/List.fs @@ -1061,6 +1061,9 @@ let randomShuffleWith (random: System.Random) (xs: 'T list) : 'T list = let randomShuffle (xs: 'T list) : 'T list = randomShuffleWith (System.Random()) xs let randomChoiceBy (randomizer: unit -> float) (xs: 'T list) : 'T = + if isEmpty xs then + invalidArg "source" SR.inputSequenceEmpty + toArray xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T list) : 'T = diff --git a/src/fable-library-ts/Random.fs b/src/fable-library-ts/Random.fs index 8b686e3e5a..a21d4b39fa 100644 --- a/src/fable-library-ts/Random.fs +++ b/src/fable-library-ts/Random.fs @@ -2,6 +2,16 @@ module Random open System +let private checkMaxValue (maxValue: int) = + if maxValue < 0 then + let msg = "maxValue ('" + string maxValue + "') must be a non-negative value." + raise <| ArgumentOutOfRangeException("maxValue", msg) + +let private checkMinMaxValue (minValue: int) (maxValue: int) = + if minValue > maxValue then + let msg = "'minValue' cannot be greater than maxValue." + raise <| ArgumentOutOfRangeException("minValue", msg) + module private Native = open Fable.Core open Fable.Core.JsInterop @@ -9,8 +19,7 @@ module private Native = let random () = JS.Math.random () let randomNext (minValue: int, maxValue: int) : int = - if (maxValue < minValue) then - raise <| ArgumentOutOfRangeException("minValue must be less than maxValue") + checkMinMaxValue minValue maxValue emitJsStatement (box () |> nonNull) @@ -48,7 +57,10 @@ type IRandom = type NonSeeded() = interface IRandom with member _.Next0() = Native.randomNext (0, Int32.MaxValue) - member _.Next1(maxValue) = Native.randomNext (0, maxValue) + + member _.Next1(maxValue) = + checkMaxValue maxValue + Native.randomNext (0, maxValue) member _.Next2(minValue, maxValue) = Native.randomNext (minValue, maxValue) @@ -145,23 +157,19 @@ type Seeded(seed: int) = let mutable d = result d <- d + float (Int32.MaxValue - 1) - d <- d / (2.0 * float (uint (Int32.MaxValue - 1))) + d <- d / (2.0 * float Int32.MaxValue - 1.0) d interface IRandom with member this.Next0() = this.InternalSample() member this.Next1(maxValue: int) = - if maxValue < 0 then - raise <| ArgumentOutOfRangeException("maxValue must be positive") - + checkMaxValue maxValue int (this.Sample() * float maxValue) member this.Next2(minValue: int, maxValue: int) = - if minValue > maxValue then - raise <| ArgumentOutOfRangeException("minValue must be less than maxValue") - - let range = int64 (maxValue - minValue) + checkMinMaxValue minValue maxValue + let range = int64 maxValue - int64 minValue if range <= int64 Int32.MaxValue then int (int (this.Sample() * float range) + minValue) @@ -177,5 +185,8 @@ type Seeded(seed: int) = for i = 0 to buffer.Length - 1 do buffer.[i] <- byte ((int (this.InternalSample())) % (int Byte.MaxValue + 1)) +let private sharedRandom = NonSeeded() + let nonSeeded () = NonSeeded() let seeded seed = Seeded(seed) +let shared () = sharedRandom diff --git a/src/fable-library-ts/Seq.fs b/src/fable-library-ts/Seq.fs index cdad084f7f..b822bd50d0 100644 --- a/src/fable-library-ts/Seq.fs +++ b/src/fable-library-ts/Seq.fs @@ -1431,16 +1431,30 @@ let randomShuffleWith (random: System.Random) (xs: 'T seq) : 'T seq = let randomShuffle (xs: 'T seq) : 'T seq = randomShuffleWith (System.Random()) xs +let private randomChoiceSource (count: int) (xs: 'T seq) : 'T[] = + if count < 0 then + invalidArg "count" (SR.inputMustBeNonNegative + "\ncount = " + string count) + + let arr = toArray xs + + if count > 0 && Array.isEmpty arr then + invalidArg "source" SR.inputSequenceEmpty + + arr + let randomChoiceBy (randomizer: unit -> float) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceBy randomizer + randomChoiceSource 1 xs |> Array.randomChoiceBy randomizer let randomChoiceWith (random: System.Random) (xs: 'T seq) : 'T = - xs |> toArray |> Array.randomChoiceWith random + randomChoiceSource 1 xs |> Array.randomChoiceWith random -let randomChoice (xs: 'T seq) : 'T = xs |> toArray |> Array.randomChoice +let randomChoice (xs: 'T seq) : 'T = + randomChoiceSource 1 xs |> Array.randomChoice +// Draws on enumeration, like FSharp.Core let randomChoicesBy (randomizer: unit -> float) (count: int) (xs: 'T seq) : 'T seq = - xs |> toArray |> Array.randomChoicesBy randomizer count |> ofArray + let arr = randomChoiceSource count xs + initialize count (fun _ -> Array.randomChoiceBy randomizer arr) let randomChoicesWith (random: System.Random) (count: int) (xs: 'T seq) : 'T seq = randomChoicesBy (fun () -> random.NextDouble()) count xs diff --git a/tests/Beam/ArrayTests.fs b/tests/Beam/ArrayTests.fs index 37ba2b0213..261a507a95 100644 --- a/tests/Beam/ArrayTests.fs +++ b/tests/Beam/ArrayTests.fs @@ -963,6 +963,41 @@ let ``test Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``test Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``test Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``test Array random functions give FSharp.Core's errors`` () = + throwsAnyError (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + raisedExceptionType (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomChoicesBy (fun () -> 0.0) -1 [|1|]) + // --- CompareWith --- [] diff --git a/tests/Beam/ListTests.fs b/tests/Beam/ListTests.fs index ddb0027a45..a3031010d9 100644 --- a/tests/Beam/ListTests.fs +++ b/tests/Beam/ListTests.fs @@ -690,6 +690,12 @@ let ``test List.randomSampleBy works`` () = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) +[] +let ``test List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + [] let ``test List.updateAt works`` () = equal [0; 2; 3; 4; 5] (List.updateAt 0 0 [1..5]) diff --git a/tests/Beam/RandomTests.fs b/tests/Beam/RandomTests.fs index 06eeacfe95..0f5a3091ef 100644 --- a/tests/Beam/RandomTests.fs +++ b/tests/Beam/RandomTests.fs @@ -112,3 +112,20 @@ let ``test System.Random.NextBytes seeded works`` () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + +[] +let ``test System.Random gives .NET's argument errors`` () = + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + rnd.Next(5, 5) |> equal 5 + +[] +let ``test System.Random.Shared works`` () = + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Beam/SeqTests.fs b/tests/Beam/SeqTests.fs index 76237f65d1..7b5d6e83b5 100644 --- a/tests/Beam/SeqTests.fs +++ b/tests/Beam/SeqTests.fs @@ -622,6 +622,20 @@ let ``test Seq.randomSampleBy works`` () = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) +[] +let ``test Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + [] let ``test Seq.average works`` () = [1.0; 2.0; 3.0] |> Seq.average |> equal 2.0 diff --git a/tests/Dart/src/ArrayTests.fs b/tests/Dart/src/ArrayTests.fs index 53ce577dc9..f2307c51a3 100644 --- a/tests/Dart/src/ArrayTests.fs +++ b/tests/Dart/src/ArrayTests.fs @@ -1438,6 +1438,56 @@ let tests () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) + testCase "Array.randomShuffleBy matches FSharp.Core" <| fun () -> + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + + testCase "Array.randomSampleBy matches FSharp.Core" <| fun () -> + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + + testCase "Array random functions draw from the given Random like FSharp.Core" <| fun () -> + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for i in 0 .. 8 do reference.Next(i, 10) |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for i in 0 .. 2 do reference.Next(0, 10 - i) |> ignore + rng.Next() |> equal (reference.Next()) + + testCase "Array random functions give FSharp.Core's error messages" <| fun () -> + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> System.Double.NaN) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> System.Double.NaN) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> System.Double.NaN) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> System.Double.NaN) 1 [|1; 2|]) + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> System.Double.NaN) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + testCase "Array.compareWith works" <| fun () -> // See #2961 let a = [|1;3|] let b = [|1;2;3|] diff --git a/tests/Dart/src/ListTests.fs b/tests/Dart/src/ListTests.fs index 09a58fc102..ccc400dc40 100644 --- a/tests/Dart/src/ListTests.fs +++ b/tests/Dart/src/ListTests.fs @@ -1064,6 +1064,16 @@ let tests() = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + testCase "List random functions match FSharp.Core" <| fun () -> + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> System.Double.NaN) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + + testCase "List.randomShuffleWith and randomSampleWith draw like Array's" <| fun () -> + List.randomShuffleWith (System.Random(42)) [0 .. 9] |> equal (List.ofArray (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|])) + List.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> equal (List.ofArray (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|])) + testCase "List.reduce and List.min on an empty list raise ArgumentException" <| fun () -> let xs: int list = [] raisedExceptionType (fun () -> List.reduce (+) xs) |> equal "ArgumentException" diff --git a/tests/Dart/src/RandomTests.fs b/tests/Dart/src/RandomTests.fs index 1f403d1ef3..7fe0212ea0 100644 --- a/tests/Dart/src/RandomTests.fs +++ b/tests/Dart/src/RandomTests.fs @@ -106,3 +106,17 @@ let tests () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + + testCase "System.Random gives .NET's argument errors" <| fun () -> + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "System.Random.Shared works" <| fun () -> + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Dart/src/SeqTests.fs b/tests/Dart/src/SeqTests.fs index f59e0f0aaa..a9ef1bf50a 100644 --- a/tests/Dart/src/SeqTests.fs +++ b/tests/Dart/src/SeqTests.fs @@ -1164,6 +1164,23 @@ let tests() = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + testCase "Seq.randomShuffleWith and randomSampleWith draw like Array's" <| fun () -> + Seq.randomShuffleWith (System.Random(42)) [0 .. 9] |> Seq.toArray |> equal (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|]) + Seq.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> Seq.toArray |> equal (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|]) + + testCase "Seq.randomChoicesBy draws on enumeration" <| fun () -> + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + testCase "Seq.reduce and Seq.min on an empty sequence raise ArgumentException" <| fun () -> let xs: int seq = Seq.empty raisedExceptionType (fun () -> Seq.reduce (+) xs) |> equal "ArgumentException" diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index ca205b7f76..533950180c 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -1651,6 +1651,64 @@ let tests = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) + testCase "Array.randomShuffleWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomShuffleWith rng [|0 .. 4|] |> equal [|3; 1; 2; 4; 0|] + rng.Next() |> equal 361709742 + let rng = System.Random(12345) + Array.randomShuffleWith rng [|0 .. 9|] |> equal [|0; 1; 8; 6; 2; 9; 3; 5; 4; 7|] + rng.Next() |> equal 1086637143 + let rng = System.Random(42) + let xs = [|0 .. 9|] + Array.randomShuffleInPlaceWith rng xs + xs |> equal [|6; 2; 3; 0; 5; 1; 8; 4; 7; 9|] + rng.Next() |> equal 1634773126 + + testCase "Array.randomShuffleBy gives .NET's results" <| fun () -> + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + + testCase "Array.randomSampleWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomSampleWith rng 3 [|0 .. 4|] |> equal [|3; 0; 4|] + rng.Next() |> equal 1122627734 + let rng = System.Random(12345) + Array.randomSampleWith rng 10 [|0 .. 29|] |> equal [|2; 29; 21; 13; 20; 25; 3; 16; 5; 10|] + rng.Next() |> equal 494509053 + let rng = System.Random(42) + Array.randomSampleWith rng 10 [|0 .. 99|] |> equal [|66; 14; 12; 52; 16; 26; 72; 51; 17; 76|] + rng.Next() |> equal 503774878 + // Draws again on a repeated index + let rng = System.Random(7) + Array.randomSampleWith rng 5 [|0 .. 21|] |> equal [|8; 19; 14; 1; 0|] + rng.Next() |> equal 2048632168 + + testCase "Array.randomSampleBy gives .NET's results" <| fun () -> + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + + testCase "Array.randomChoicesWith gives .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Array.randomChoicesWith rng 4 [|0 .. 9|] |> equal [|6; 1; 1; 5|] + rng.Next() |> equal 361709742 + + testCase "Array random functions give FSharp.Core's error messages" <| fun () -> + let nanMessage = "The index is outside the legal range.\nrandomizer returned NaN, should be in range [0.0, 1.0). (Parameter 'randomizer')" + throwsError nanMessage (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomShuffleInPlaceBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsError nanMessage (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + raisedExceptionType (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) |> equal "ArgumentOutOfRangeException" + throwsError "The index is outside the legal range.\nrandomizer returned 1, should be in range [0.0, 1.0). (Parameter 'randomizer')" (fun () -> Array.randomChoiceBy (fun () -> 1.0) [|1; 2|]) + throwsError "The input array was empty. (Parameter 'source')" (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> Array.randomSampleWith (System.Random(1)) 1 ([||]: int[])) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Array.randomChoicesWith (System.Random(1)) -1 [|1|]) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + testCase "Array.compareWith works" <| fun () -> // See #2961 let a = [|1;3|] let b = [|1;2;3|] diff --git a/tests/Js/Main/ListTests.fs b/tests/Js/Main/ListTests.fs index e8f2d8c14a..17e8cd3fc5 100644 --- a/tests/Js/Main/ListTests.fs +++ b/tests/Js/Main/ListTests.fs @@ -1076,6 +1076,21 @@ let tests = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + testCase "List random functions give .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + List.randomShuffleWith rng [0 .. 4] |> equal [3; 1; 2; 4; 0] + rng.Next() |> equal 361709742 + let rng = System.Random(42) + List.randomSampleWith rng 10 [0 .. 99] |> equal [66; 14; 12; 52; 16; 26; 72; 51; 17; 76] + rng.Next() |> equal 503774878 + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + + testCase "List random functions give FSharp.Core's error messages" <| fun () -> + throwsError "The index is outside the legal range.\nrandomizer returned NaN, should be in range [0.0, 1.0). (Parameter 'randomizer')" (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> List.randomChoicesWith (System.Random(1)) -1 [1]) + testCase "List functions raise ArgumentException on empty lists" <| fun () -> let xs: int list = [] raisedExceptionType (fun () -> List.head xs) |> equal "ArgumentException" diff --git a/tests/Js/Main/RandomTests.fs b/tests/Js/Main/RandomTests.fs index 5a56c1a019..97cb7bc59d 100644 --- a/tests/Js/Main/RandomTests.fs +++ b/tests/Js/Main/RandomTests.fs @@ -84,6 +84,27 @@ let tests = throwsAnyError <| fun () -> rnd.Next(14, 10) throwsAnyError <| fun () -> rnd.NextBytes(null) + testCase "System.Random seeded Next works for ranges above Int32.MaxValue" <| fun () -> + let rnd = Random(42) + rnd.Next(Int32.MinValue, Int32.MaxValue) |> equal 1434747709 + rnd.Next(Int32.MinValue, Int32.MaxValue) |> equal -269548476 + let rnd = Random(0) + [ for _ in 1 .. 3 -> rnd.Next(-1000000000, 2000000000) ] |> equal [-589364906; 1652034034; 809049730] + + testCase "System.Random gives .NET's argument errors" <| fun () -> + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsError "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + + testCase "System.Random.Shared works" <| fun () -> + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true + testCase "System.Random seeded passes chi-squared test" <| fun () -> let rnd = Random(2026) let statistic = chiSquaredStatistic 16 10000 (fun () -> rnd.Next(16)) diff --git a/tests/Js/Main/SeqTests.fs b/tests/Js/Main/SeqTests.fs index 418eec38c9..4a46414ec6 100644 --- a/tests/Js/Main/SeqTests.fs +++ b/tests/Js/Main/SeqTests.fs @@ -1007,6 +1007,28 @@ let tests = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + testCase "Seq random functions give .NET's results for the same seed" <| fun () -> + let rng = System.Random(42) + Seq.randomShuffleWith rng [0 .. 4] |> Seq.toList |> equal [3; 1; 2; 4; 0] + rng.Next() |> equal 361709742 + let rng = System.Random(7) + Seq.randomSampleWith rng 5 [0 .. 21] |> Seq.toList |> equal [8; 19; 14; 1; 0] + rng.Next() |> equal 2048632168 + + testCase "Seq.randomChoicesWith draws on enumeration" <| fun () -> + let rng = System.Random(42) + let xs = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal 1434747710 + xs |> Seq.toList |> equal [1; 1; 5] + xs |> Seq.toList |> equal [1; 2; 7] + let mutable calls = 0 + let ys = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + ys |> Seq.toList |> equal [5; 5; 5] + calls |> equal 3 + throwsError "The input must be non-negative.\ncount = -1 (Parameter 'count')" (fun () -> Seq.randomChoicesWith rng -1 [1]) + throwsError "The input sequence was empty. (Parameter 'source')" (fun () -> Seq.randomChoicesWith rng 1 Seq.empty) + testCase "Seq.item throws exception when index is out of range" <| fun () -> let xs = [0] (try Seq.item 1 xs |> ignore; false with | _ -> true) |> equal true diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index c1f68a3c12..7333a46076 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -1603,6 +1603,59 @@ let ``test Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``test Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``test Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``test Array random functions draw from the given Random`` () = + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for _ in 1 .. 9 do reference.NextDouble() |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for _ in 1 .. 3 do reference.NextDouble() |> ignore + rng.Next() |> equal (reference.Next()) + +[] +let ``test Array random functions give FSharp.Core's error messages`` () = + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + [] let ``test Int64 arrays roundtrip through their backing storage`` () = let xs = [| 1L; -2L; 3L |] diff --git a/tests/Python/TestList.fs b/tests/Python/TestList.fs index c8631a6af8..3a82d99dd4 100644 --- a/tests/Python/TestList.fs +++ b/tests/Python/TestList.fs @@ -1240,6 +1240,13 @@ let ``test List.randomSampleBy works`` () = throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) +[] +let ``test List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + [] let ``test List.collect works II`` () = let xs = ["a"; "fable"; "bar" ] diff --git a/tests/Python/TestRandom.fs b/tests/Python/TestRandom.fs index 06eeacfe95..0f5a3091ef 100644 --- a/tests/Python/TestRandom.fs +++ b/tests/Python/TestRandom.fs @@ -112,3 +112,20 @@ let ``test System.Random.NextBytes seeded works`` () = Random(5432).NextBytes(buffer1) Random(5432).NextBytes(buffer2) buffer1 |> equal buffer2 + +[] +let ``test System.Random gives .NET's argument errors`` () = + for rnd in [Random(42); Random()] do + throwsErrorContaining "maxValue ('-1') must be a non-negative value. (Parameter 'maxValue')" (fun () -> rnd.Next(-1)) + throwsErrorContaining "'minValue' cannot be greater than maxValue. (Parameter 'minValue')" (fun () -> rnd.Next(5, 3)) + raisedExceptionType (fun () -> rnd.Next(-1)) |> equal "ArgumentOutOfRangeException" + raisedExceptionType (fun () -> rnd.Next(5, 3)) |> equal "ArgumentOutOfRangeException" + rnd.Next(5, 5) |> equal 5 + +[] +let ``test System.Random.Shared works`` () = + let x = Random.Shared.Next(5) + (x >= 0 && x < 5) |> equal true + let x = Random.Shared.NextDouble() + (x >= 0.0 && x < 1.0) |> equal true + obj.ReferenceEquals(Random.Shared, Random.Shared) |> equal true diff --git a/tests/Python/TestSeq.fs b/tests/Python/TestSeq.fs index ecacf82993..cc5c435a83 100644 --- a/tests/Python/TestSeq.fs +++ b/tests/Python/TestSeq.fs @@ -1320,6 +1320,20 @@ let ``test Seq.randomSampleBy works`` () = throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) +[] +let ``test Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) + [] let ``test Seq.reduce and Seq.reduceBack raise ArgumentException on empty input`` () = let xs = Seq.empty diff --git a/tests/Rust/tests/src/ArrayTests.fs b/tests/Rust/tests/src/ArrayTests.fs index b860351d83..ec2404f8c0 100644 --- a/tests/Rust/tests/src/ArrayTests.fs +++ b/tests/Rust/tests/src/ArrayTests.fs @@ -1452,6 +1452,59 @@ let ``Array.randomSampleBy works`` () = throwsAnyError (fun () -> Array.randomSampleBy (fun () -> 1.0) 1 [|1; 2|] |> ignore) throwsAnyError (fun () -> Array.randomSampleBy (fun () -> -0.1) 1 [|1; 2|] |> ignore) +[] +let ``Array.randomShuffleBy matches FSharp.Core`` () = + Array.randomShuffleBy (fun () -> 0.0) [|0 .. 4|] |> equal [|0; 1; 2; 3; 4|] + Array.randomShuffleBy (fun () -> 0.999) [|0 .. 4|] |> equal [|4; 0; 1; 2; 3|] + let xs = [|0 .. 4|] + Array.randomShuffleInPlaceBy (fun () -> 0.999) xs + xs |> equal [|4; 0; 1; 2; 3|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + float ((calls * 37) % 100) / 100.0 + Array.randomShuffleBy next [|0 .. 9|] |> equal [|3; 7; 2; 6; 9; 0; 8; 4; 5; 1|] + calls |> equal 9 + +[] +let ``Array.randomSampleBy matches FSharp.Core`` () = + Array.randomSampleBy (fun () -> 0.0) 3 [|0 .. 4|] |> equal [|0; 4; 3|] + Array.randomSampleBy (fun () -> 0.999) 3 [|0 .. 4|] |> equal [|4; 3; 2|] + Array.randomSampleBy (fun () -> 0.0) 0 ([||]: int[]) |> equal [||] + // Above the max set size it draws again on a repeated index + let values = [|0.0; 0.0; 0.5|] + let mutable calls = 0 + let next () = + calls <- calls + 1 + values[calls - 1] + Array.randomSampleBy next 2 [|0 .. 21|] |> equal [|0; 11|] + calls |> equal 3 + +[] +let ``Array random functions draw from the given Random like FSharp.Core`` () = + let rng = System.Random(42) + let shuffled = Array.randomShuffleWith rng [|0 .. 9|] + Array.randomShuffleWith (System.Random(42)) [|0 .. 9|] |> equal shuffled + let reference = System.Random(42) + for i in 0 .. 8 do reference.Next(i, 10) |> ignore + rng.Next() |> equal (reference.Next()) + let rng = System.Random(42) + let sample = Array.randomSampleWith rng 3 [|0 .. 9|] + Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|] |> equal sample + let reference = System.Random(42) + for i in 0 .. 2 do reference.Next(0, 10 - i) |> ignore + rng.Next() |> equal (reference.Next()) + +[] +let ``Array random functions give FSharp.Core's error messages`` () = + let message = "randomizer returned NaN, should be in range [0.0, 1.0)." + throwsErrorContaining message (fun () -> Array.randomShuffleBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoiceBy (fun () -> nan) [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomChoicesBy (fun () -> nan) 2 [|1; 2|]) + throwsErrorContaining message (fun () -> Array.randomSampleBy (fun () -> nan) 1 [|1; 2|]) + throwsErrorContaining "The input array was empty." (fun () -> Array.randomChoiceWith (System.Random(1)) ([||]: int[])) + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Array.randomSampleBy (fun () -> 0.0) -1 [|1|]) + [] let ``Array.transpose works`` () = // integer array diff --git a/tests/Rust/tests/src/ListTests.fs b/tests/Rust/tests/src/ListTests.fs index 456d27e883..63b3fa51ce 100644 --- a/tests/Rust/tests/src/ListTests.fs +++ b/tests/Rust/tests/src/ListTests.fs @@ -1324,3 +1324,15 @@ let ``List.randomSampleBy works`` () = // Out-of-range randomizer should throw throwsAnyError (fun () -> List.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> ignore) throwsAnyError (fun () -> List.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> ignore) + +[] +let ``List random functions match FSharp.Core`` () = + List.randomShuffleBy (fun () -> 0.999) [0 .. 4] |> equal [4; 0; 1; 2; 3] + List.randomSampleBy (fun () -> 0.0) 3 [0 .. 4] |> equal [0; 4; 3] + throwsErrorContaining "randomizer returned NaN, should be in range [0.0, 1.0)." (fun () -> List.randomChoiceBy (fun () -> nan) [1; 2]) + throwsErrorContaining "The input sequence was empty." (fun () -> List.randomChoiceWith (System.Random(1)) ([]: int list)) + +[] +let ``List.randomShuffleWith and randomSampleWith draw like Array's`` () = + List.randomShuffleWith (System.Random(42)) [0 .. 9] |> equal (List.ofArray (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|])) + List.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> equal (List.ofArray (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|])) diff --git a/tests/Rust/tests/src/SeqTests.fs b/tests/Rust/tests/src/SeqTests.fs index 5f07d3d78e..43500ec317 100644 --- a/tests/Rust/tests/src/SeqTests.fs +++ b/tests/Rust/tests/src/SeqTests.fs @@ -1412,3 +1412,22 @@ let ``Seq.randomSampleBy works`` () = Array.distinct sample |> Array.length |> equal 3 throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> 1.0) 1 [1; 2] |> Seq.toArray |> ignore) throwsAnyError (fun () -> Seq.randomSampleBy (fun () -> -0.1) 1 [1; 2] |> Seq.toArray |> ignore) + +[] +let ``Seq.randomShuffleWith and randomSampleWith draw like Array's`` () = + Seq.randomShuffleWith (System.Random(42)) [0 .. 9] |> Seq.toArray |> equal (Array.randomShuffleWith (System.Random(42)) [|0 .. 9|]) + Seq.randomSampleWith (System.Random(42)) 3 [0 .. 9] |> Seq.toArray |> equal (Array.randomSampleWith (System.Random(42)) 3 [|0 .. 9|]) + +[] +let ``Seq.randomChoicesBy draws on enumeration`` () = + let mutable calls = 0 + let xs = Seq.randomChoicesBy (fun () -> calls <- calls + 1; 0.5) 3 [0 .. 9] + calls |> equal 0 + xs |> Seq.toList |> equal [5; 5; 5] + xs |> Seq.toList |> equal [5; 5; 5] + calls |> equal 6 + let rng = System.Random(42) + let ys = Seq.randomChoicesWith rng 3 [0 .. 9] + rng.Next() |> equal (System.Random(42).Next()) + ys |> Seq.length |> equal 3 + throwsErrorContaining "The input must be non-negative.\ncount = -1" (fun () -> Seq.randomChoicesWith rng -1 [1]) From 24e01fd30e3eec7571816b4d6cc9f6281cce0de4 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 23:29:35 +0100 Subject: [PATCH 285/322] fix(js/ts): unescape doubled braces in String.Format (#5231) --- src/fable-library-ts/String.ts | 7 +++++-- tests/Js/Main/StringTests.fs | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 15a5aa2c39..ce0c665cfd 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -21,7 +21,7 @@ function formatDateLike(rep: any, format: string | undefined): string { const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\*|\d+)?(?:\.(\d+))?(\w)/g; const interpolateRegExp = /(?:(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w))?%P\(\)/g; -const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; +const formatRegExp = /\{\{|\}\}|\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g; function isLessThan(x: Numeric, y: number) { return numericCompare(x, y) < 0; @@ -424,7 +424,10 @@ export function format(str: string | object, ...args: any[]) { str2 = str; } - return str2.replace(formatRegExp, (_, idx: number, padLength, format, precision, pattern) => { + return str2.replace(formatRegExp, (match, idx: number, padLength, format, precision, pattern) => { + if (match === "{{" || match === "}}") { + return match[0]; + } if (idx < 0 || idx >= args.length) { throw new Exception("Index must be greater or equal to zero and less than the arguments' length.") } diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index b32397991e..1f4210e6ee 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -706,6 +706,13 @@ let tests = testList "Strings" [ .Replace(",", ".").Replace(" %", "%") |> equal "0.55 54.67% 2014-09-26 00:19" + testCase "String.Format unescapes doubled braces" <| fun () -> // See #5163 + String.Format(CultureInfo.InvariantCulture, "{{{0}}}", 5) |> equal "{5}" + String.Format(CultureInfo.InvariantCulture, "{{x}} {0}", 1) |> equal "{x} 1" + String.Format(CultureInfo.InvariantCulture, "}}{{", 1) |> equal "}{" + String.Format(CultureInfo.InvariantCulture, "{{{0:N1}}}", 2.26) |> equal "{2.3}" + String.Format(CultureInfo.InvariantCulture, "{{{0,4}}}", 5) |> equal "{ 5}" + // String.Format has to recognise the value at runtime. The default JS // representation passes a Date for DateOnly and a number for TimeOnly and // TimeSpan, which are indistinguishable from a DateTime and a float, so only From a7dcab8d770ac56f825e15361df7b2e05f618e5e Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 23:29:49 +0100 Subject: [PATCH 286/322] fix(js/ts): treat null as empty in String.Join and Concat (#5230) --- src/fable-library-ts/String.ts | 8 ++++---- tests/Js/Main/StringTests.fs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index ce0c665cfd..79080b39a0 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -647,14 +647,14 @@ export function isNullOrWhiteSpace(str: string | any) { } export function concat(...xs: any[]): string { - return xs.map((x) => String(x)).join(""); + return xs.map((x) => (x == null ? "" : String(x))).join(""); } export function join(delimiter: string, xs: Iterable): string { if (Array.isArray(xs)) { - return xs.join(delimiter); + return xs.join(delimiter ?? ""); } else { - return Array.from(xs).join(delimiter); + return Array.from(xs).join(delimiter ?? ""); } } @@ -663,7 +663,7 @@ export function joinWithIndices(delimiter: string, xs: string[], startIndex: num if (endIndexPlusOne > xs.length) { throw new Exception("Index and count must refer to a location within the buffer."); } - return xs.slice(startIndex, endIndexPlusOne).join(delimiter); + return xs.slice(startIndex, endIndexPlusOne).join(delimiter ?? ""); } function notSupported(name: string): never { diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 1f4210e6ee..2f513d6f41 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -1272,6 +1272,22 @@ let tests = testList "Strings" [ String.Join(",", [|"abc"|]) |> equal "abc" String.Join(",", ["abc"]) |> equal "abc" + testCase "String.Join treats a null separator as empty" <| fun () -> // See #5167 + String.Join((null: string), [| "a"; "b" |]) |> equal "ab" + String.Join((null: string), seq { yield "a"; yield "b" }) |> equal "ab" + String.Join((null: string), "a", "b") |> equal "ab" + String.Join((null: string), [| "a"; "b"; "c" |], 1, 2) |> equal "bc" + + testCase "String.Join treats null elements as empty" <| fun () -> + String.Join(",", [| "a"; null; "b" |]) |> equal "a,,b" + String.Join(",", [| box "a"; null; box 1 |]) |> equal "a,,1" + + testCase "System.String.Concat treats null arguments as empty" <| fun () -> // See #5167 + String.Concat("a", (null: string), "b") |> equal "ab" + String.Concat((null: string), "b") |> equal "b" + String.Concat([| "a"; null; "b" |]) |> equal "ab" + String.Concat(box "a", null, box 1) |> equal "a1" + testCase "System.String.Concat works" <| fun () -> String.Concat("a", "b", "c") |> equal "abc" From 7319a994a9c3be7d31348d29d2c464482b5f21f9 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Fri, 30 Oct 2026 23:34:25 +0100 Subject: [PATCH 287/322] fix(js/ts/python): clamp negative slice bounds like .NET (#5232) --- src/Fable.Transforms/Python/Replacements.fs | 18 +++++++++++++++++- src/Fable.Transforms/Replacements.fs | 18 +++++++++++++++++- src/fable-library-py/fable_library/util.py | 6 ++++++ tests/Js/Main/ArrayTests.fs | 15 +++++++++++++++ tests/Js/Main/StringTests.fs | 11 +++++++++++ tests/Python/TestArray.fs | 16 ++++++++++++++++ tests/Python/TestString.fs | 12 ++++++++++++ 7 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index b83489bc1f..66d15d3596 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2676,10 +2676,26 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "GetArray", _, [ ar; idx ] -> getExpr r t ar idx |> Some | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // Python slicing counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.LibCall(com, "util", "slice_index", Int32.Number, [ index ], ?loc = r) + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> Helper.GlobalCall("len", t, [ ar ], [ t ], ?loc = r) - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index b80a88f6e4..9c640fc58c 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -3096,10 +3096,26 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "GetArray", _, [ ar; idx ] -> arrayGetItem com r t [ idx; ar ] (i.SignatureArgTypes |> List.rev) | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some | ("GetArraySlice" | "GetStringSlice"), None, [ ar; lower; upper ] -> + // JS slice counts a negative index from the end, .NET clamps it to 0 + let clampToZero (index: Expr) = + match index with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> index + | _ -> Helper.GlobalCall("Math", Int32.Number, [ index; makeIntConst 0 ], memb = "max") + + let lower = + match lower with + | Value(NewOption(None, _, _), _) -> makeIntConst 0 + | Value(NewOption(Some lower, _, _), _) + | lower -> clampToZero lower + let upper = match upper with | Value(NewOption(None, _, _), _) -> getExpr None (Int32.Number) ar (makeStrConst "length") - | _ -> add upper (makeIntConst 1) + | Value(NewOption(Some upper, _, _), _) + | upper -> + match upper with + | Value(NumberConstant(NumberValue.Int32 n, _), _) when n >= 0 -> makeIntConst (n + 1) + | upper -> add upper (makeIntConst 1) |> clampToZero Helper.InstanceCall(ar, "slice", t, [ lower; upper ], ?loc = r) |> Some | "SetArraySlice", None, args -> diff --git a/src/fable-library-py/fable_library/util.py b/src/fable-library-py/fable_library/util.py index 7bb0f065eb..eda490d628 100644 --- a/src/fable-library-py/fable_library/util.py +++ b/src/fable-library-py/fable_library/util.py @@ -762,6 +762,11 @@ def random_shared() -> random.Random: return _shared_random +def slice_index(index: int) -> int: + """Clamp a slice bound to 0, as .NET does where Python would count from the end.""" + return index if index > 0 else 0 + + def random_int(rand: random.Random, a: int, b: int) -> int: if a == b: return int32(a) @@ -990,6 +995,7 @@ def range(start: int, stop: int, step: int = 1) -> Iterable[int]: "random_shared", "range", "round", + "slice_index", "to_iterable", "unescape_data_string", ] diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 533950180c..2fd87be533 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -230,6 +230,21 @@ let tests = a.[3..3] <- [|999|] a |> equal [|999; 2; 3; 999; 5; 6|] + testCase "Array slices clamp the bounds like .NET" <| fun () -> // See #5164 + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + testCase "Array literals work" <| fun () -> let x = [| 1; 2; 3; 4; 5 |] equal 5 x.Length diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index 2f513d6f41..ce1e039aa0 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -694,6 +694,17 @@ let tests = testList "Strings" [ sprintf "%s" s.[2..] |> equal "t and dog" sprintf "%s" s.[..8] |> equal "cat and d" + testCase "String slices clamp the bounds like .NET" <| fun () -> // See #5164 + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab" + testCase "String.Format works" <| fun () -> let arg1, arg2, arg3 = "F#", "Fable", "Babel" String.Format(CultureInfo.InvariantCulture, "{2} is to {1} what {1} is to {0}", arg1, arg2, arg3) diff --git a/tests/Python/TestArray.fs b/tests/Python/TestArray.fs index 7333a46076..1d85ae5af3 100644 --- a/tests/Python/TestArray.fs +++ b/tests/Python/TestArray.fs @@ -248,6 +248,22 @@ let ``test Array slice with non-numeric arrays work`` () = equal xs.[2] "X" equal xs.[3] "D" +[] +let ``test Array slices clamp the bounds like .NET`` () = + let xs = [| 1; 2; 3 |] + xs.[-1..1] |> equal [| 1; 2 |] + xs.[-5..] |> equal [| 1; 2; 3 |] + xs.[1..5] |> equal [| 2; 3 |] + xs.[..5] |> equal [| 1; 2; 3 |] + xs.[.. -2] |> equal [||] + xs.[-5.. -1] |> equal [||] + xs.[5..6] |> equal [||] + xs.[2..1] |> equal [||] + let lower, upper = -1, 1 + xs.[lower..upper] |> equal [| 1; 2 |] + let strings = [| "a"; "b"; "c" |] + strings.[-1..1] |> equal [| "a"; "b" |] + [] let ``test Array literals work`` () = let x = [| 1; 2; 3; 4; 5 |] diff --git a/tests/Python/TestString.fs b/tests/Python/TestString.fs index 6bc2e2c975..95ea6e9ed3 100644 --- a/tests/Python/TestString.fs +++ b/tests/Python/TestString.fs @@ -1215,3 +1215,15 @@ let ``test unicode categories`` () = Char.IsLetterOrDigit c |> ignore ) #endif + +[] +let ``test String slices clamp the bounds like .NET`` () = + let s = "abc" + s.[-1..1] |> equal "ab" + s.[-5..] |> equal "abc" + s.[1..5] |> equal "bc" + s.[.. -2] |> equal "" + s.[-3.. -2] |> equal "" + s.[2..1] |> equal "" + let lower, upper = -1, 1 + s.[lower..upper] |> equal "ab" From 705b6b70bf0b732811bd6e69cfc8fc0614b831c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:59:35 -0700 Subject: [PATCH 288/322] fix(rust): Inline hashing, more Decimal tests (#5236) --- src/Fable.Transforms/Rust/Fable2Rust.fs | 4 +- src/Fable.Transforms/Rust/Replacements.fs | 171 +++-- src/Fable.Transforms/Transforms.Util.fs | 28 + src/fable-library-rust/Cargo.toml | 2 +- src/fable-library-rust/src/BigInt.rs | 7 +- src/fable-library-rust/src/DateOnly.rs | 4 +- src/fable-library-rust/src/DateTime.rs | 5 +- src/fable-library-rust/src/DateTimeOffset.rs | 5 +- src/fable-library-rust/src/Decimal.rs | 620 ++++++++++++++++-- .../src/Fable.Library.Rust.fsproj | 1 + src/fable-library-rust/src/Guid.rs | 4 +- src/fable-library-rust/src/Interop.rs | 2 + src/fable-library-rust/src/LrcPtr.rs | 2 +- src/fable-library-rust/src/Mutable.rs | 1 + src/fable-library-rust/src/Native.rs | 20 +- src/fable-library-rust/src/String.rs | 4 +- .../src/System.Globalization.fs | 6 + src/fable-library-rust/src/TimeOnly.rs | 4 +- src/fable-library-rust/src/TimeSpan.rs | 4 +- .../vendored/hashable/src/lib.rs | 3 +- src/quicktest-rust/src/main.fs | 46 +- tests/Rust/tests/src/ArithmeticTests.fs | 383 ++++++++++- tests/Rust/tests/src/ConvertTests.fs | 40 ++ tests/Rust/tests/src/RuneTests.fs | 4 +- 24 files changed, 1164 insertions(+), 206 deletions(-) create mode 100644 src/fable-library-rust/src/System.Globalization.fs diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 1dbcb27e8a..efeb1566ae 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -5565,11 +5565,13 @@ module Util = // } // impl Eq for {self_ty} {} // impl core::hash::Hash for {self_ty} { + // #[inline] // fn hash(&self, state: &mut H) { // core::hash::Hash::hash(&(self as *const Self as usize), state) // } // } // impl Native_::Hashable for {self_ty} { + // #[inline] // fn getHashCode(&self) -> i32 { Native_::referenceHash(self) } // } @@ -5611,7 +5613,7 @@ module Util = let generics = [ hParam ] |> mkGenerics let fnKind = mkFnKind DEFAULT_FN_HEADER fnDecl generics fnBody - mkFnAssocItem [] "hash" fnKind + mkFnAssocItem [ mkAttr "inline" [] ] "hash" fnKind let getHashCodeFnItem = let referenceHashName = getLibraryImportName com ctx "Native" "referenceHash" diff --git a/src/Fable.Transforms/Rust/Replacements.fs b/src/Fable.Transforms/Rust/Replacements.fs index 3e36ebafd7..0936064146 100644 --- a/src/Fable.Transforms/Rust/Replacements.fs +++ b/src/Fable.Transforms/Rust/Replacements.fs @@ -439,6 +439,20 @@ let applyOp (com: ICompiler) (ctx: Context) r t opName (args: Expr list) = match argTypes with // | Number(BigInt as kind,_)::_ -> // libCallTyped com r t "BigInt" opName [ left; right ] argTypes + | Number(Decimal, _) :: _ -> + let decimalMethod = + match opName with + | Operators.addition -> Some "add" + | Operators.subtraction -> Some "subtract" + | Operators.multiply -> Some "multiply" + | Operators.division -> Some "divide" + | Operators.modulus -> Some "remainder" + | Operators.unaryNegation -> Some "negate" + | _ -> None + + match decimalMethod with + | Some meth -> libCallTyped com r t "Decimal" meth args argTypes + | None -> nativeOp opName argTypes args | Builtin(BclDateTime | BclDateTimeOffset | BclTimeOnly | BclTimeSpan | BclRune) :: _ -> nativeOp opName argTypes args | Builtin(FSharpSet _) :: _ -> @@ -474,25 +488,23 @@ let isCompatibleWithNativeComparison = // * `.GetHashCode` called directly defaults to identity hash (for reference types except string) if not implemented. // * `LanguagePrimitive.PhysicalHash` creates an identity hash no matter whether GetHashCode is implemented or not. -let referenceHash (com: ICompiler) ctx r (arg: Expr) = +let referenceHash (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with | Boolean | Char | String - | Number _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) - | _ -> libCall com r Int32.Number "Native" "referenceHash" [ makeRef arg ] + | Number _ -> libCall com r t "Native" "getHashCode" [ makeRef arg ] + | _ -> libCall com r t "Native" "referenceHash" [ makeRef arg ] -let getHashCode (com: ICompiler) ctx r (arg: Expr) = +let getHashCode (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with - | HasReferenceEquality com _ -> referenceHash com ctx r arg - | Builtin BclRune -> libCall com r Int32.Number "Rune" "getHashCode" [ arg ] - | Char -> libCall com r Int32.Number "Char" "GetHashCode" [ arg ] - | _ -> Helper.InstanceCall(arg, "getHashCode", Int32.Number, [], [], [], ?loc = r) + | HasReferenceEquality com _ -> referenceHash com ctx r t arg + | _ -> libCall com r t "Native" "getHashCode" [ makeRef arg ] -let objectHash (com: ICompiler) ctx r (arg: Expr) = +let objectHash (com: ICompiler) ctx r t (arg: Expr) = match arg.Type with - | Array _ -> referenceHash com ctx r arg - | _ -> getHashCode com ctx r arg + | Array _ -> referenceHash com ctx r t arg + | _ -> getHashCode com ctx r t arg let referenceEquals (com: ICompiler) ctx r (left: Expr) (right: Expr) = match left, right with @@ -1199,6 +1211,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | "Round", _ -> match args with | [ ExprType(Number(Decimal, _)) ] -> libCallThis com r t i "Decimal" "round" thisArg args |> Some + | [ ExprType(Number(Decimal, _)); ExprTypeEnum "System.MidpointRounding" _ ] -> + makeLibCall com r t i "Decimal" "roundMode" args |> Some | [ ExprType(Number(Decimal, _)); ExprType(Number(Int32, _)) ] -> libCallThis com r t i "Decimal" "roundTo" thisArg args |> Some | [ ExprType(Number(Decimal, _)); mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some @@ -1245,7 +1259,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o // Concatenates two lists | "op_Append", _ -> libCallThis com r t i "List" "append" thisArg args |> Some | "IsNull", [ arg ] -> nullCheck r true arg |> Some - | "Hash", [ arg ] -> getHashCode com ctx r arg |> Some + | "Hash", [ arg ] -> getHashCode com ctx r t arg |> Some // Comparison | Patterns.SetContains Operators.compareSet, [ left; right ] -> applyCompareOp com ctx r t i.CompiledName left right |> Some @@ -1255,7 +1269,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o match args.Head.Type with | Boolean | Char - | String + | String -> math r t args i.SignatureArgTypes i.CompiledName |> Some + | Number(Decimal, _) -> libCall com r t "Decimal" (Naming.lowerFirst meth) args |> Some | Number _ -> math r t args i.SignatureArgTypes i.CompiledName |> Some | _ -> libCall com r t "Native" (Naming.lowerFirst meth) args |> Some | ("MinMagnitude" | "MaxMagnitude") as meth, _ -> @@ -1285,7 +1300,7 @@ let objects (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | Array _, _ | _, Array _ -> referenceEquals com ctx r arg1 arg2 |> Some | _ -> TypeCast(arg2, arg1.Type) |> equals com ctx r arg1 |> Some - | "GetHashCode", Some arg, _ -> objectHash com ctx r arg |> Some + | "GetHashCode", Some arg, _ -> objectHash com ctx r t arg |> Some | "GetType", Some arg, _ -> if arg.Type = Any then // Dynamic type of a boxed value: resolve via the runtime reflection @@ -1302,7 +1317,7 @@ let valueTypes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr | "ToString", Some arg, _ -> toString com ctx r [ arg ] |> Some | "Equals", Some arg1, [ arg2 ] | "Equals", None, [ arg1; arg2 ] -> equals com ctx r arg1 arg2 |> Some - | "GetHashCode", Some arg, _ -> getHashCode com ctx r arg |> Some + | "GetHashCode", Some arg, _ -> getHashCode com ctx r t arg |> Some | "CompareTo", Some arg1, [ arg2 ] | "Compare", None, [ arg1; arg2 ] -> compare com ctx r arg1 arg2 |> Some | _ -> None @@ -1555,45 +1570,43 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt | [ ExprTypeAs(String, arg1) ] -> libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some - | [ ExprTypeAs(String, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> + | [ ExprTypeAs(String, arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some - | [ ExprTypeAs(String, arg1) - ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> + | [ ExprTypeAs(String, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeEnum Types.stringSplitOptions arg3 ] -> libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ] -> libCall com r t "String" "split" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some - | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> + | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _); ExprTypeEnum Types.stringSplitOptions arg2 ] -> libCall com r t "String" "split" [ c; arg1; makeIntConst -1; arg2 ] |> Some - | [ ExprTypeAs(Array(String, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> + | [ ExprTypeAs(Array(String, _), arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> libCall com r t "String" "splitStrings" [ c; arg1; arg2 ] |> Some | [ Value(NewArray(ArrayValues [ arg1 ], String, _), _) ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> + ExprTypeEnum Types.stringSplitOptions arg3 ] -> libCall com r t "String" "split" [ c; arg1; arg2; arg3 ] |> Some | [ ExprTypeAs(Char, arg1) ] -> libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; makeIntConst 0 ] |> Some - | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> + | [ ExprTypeAs(Char, arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; makeIntConst -1; arg2 ] |> Some - | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> + | [ ExprTypeAs(Char, arg1); ExprTypeAs(Number(Int32, _), arg2); ExprTypeEnum Types.stringSplitOptions arg3 ] -> libCall com r t "String" "splitChars" [ c; makeArray Char [ arg1 ]; arg2; arg3 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ] -> libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; makeIntConst 0 ] |> Some - | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg2) ] -> + | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeEnum Types.stringSplitOptions arg2 ] -> libCall com r t "String" "splitChars" [ c; arg1; makeIntConst -1; arg2 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1); ExprTypeAs(Number(Int32, _), arg2) ] -> libCall com r t "String" "splitChars" [ c; arg1; arg2; makeIntConst 0 ] |> Some | [ ExprTypeAs(Array(Char, _), arg1) ExprTypeAs(Number(Int32, _), arg2) - ExprTypeAs(Number(_, NumberInfo.IsEnum _), arg3) ] -> + ExprTypeEnum Types.stringSplitOptions arg3 ] -> libCall com r t "String" "splitChars" [ c; arg1; arg2; arg3 ] |> Some // Remaining gap: the count-bearing string[] overload Split(string[], int, options) @@ -2114,37 +2127,7 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None -/// Drops culture and style arguments from a call. -/// -/// Rust's formatting and parsing are invariant, which is exactly what -/// InvariantCulture asks for, so the overloads that take a provider are -/// equivalent to the ones that do not. Passing the argument on was not: the -/// runtime functions have no parameter for it, so `Decimal.Parse(s, style, -/// culture)` reached a one-argument function and `d.ToString(culture)` put a -/// provider where a format string belongs. -/// -/// Filtering by argument type rather than by position leaves the arity of every -/// other overload alone. -let dropCultureArgs (args: Expr list) = - let isCultureOrStyle (e: Expr) = - match e.Type with - | DeclaredType(ent, _) -> - match ent.FullName with - | Types.cultureInfo - | "System.IFormatProvider" -> true - | _ -> false - | Number(_, NumberInfo.IsEnum ent) -> - match ent.FullName with - | "System.Globalization.NumberStyles" - | "System.Globalization.DateTimeStyles" -> true - | _ -> false - | _ -> false - - args |> List.filter (isCultureOrStyle >> not) - let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = - let args = dropCultureArgs args - match i.CompiledName, thisArg, args with | (".ctor" | "MakeDecimal"), _, ([ low; mid; high; isNegative; scale ] as args) -> makeLibCall com r t i "Decimal" "fromParts" args |> Some @@ -2152,6 +2135,21 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: makeLibCall com r t i "Decimal" "fromInts" args |> Some | ".ctor", _, [ arg ] -> convertTo com ctx r t args |> Some | "GetBits", _, _ -> makeLibCall com r t i "Decimal" "getBits" args |> Some + | "Parse", _, [ input; ExprTypeEnum Types.numberStyles style; FormatProvider _ ] -> + makeLibCall com r t i "Decimal" "parseWithStyle" [ input; style ] |> Some + | "Parse", _, [ input; FormatProvider _ ] -> makeLibCall com r t i "Decimal" "parse" [ input ] |> Some + | "Parse", _, [ input; ExprTypeEnum Types.numberStyles style ] -> + makeLibCall com r t i "Decimal" "parseWithStyle" [ input; style ] |> Some + | "Parse", _, [ input ] -> makeLibCall com r t i "Decimal" "parse" args |> Some + | "TryParse", _, [ input; ExprTypeEnum Types.numberStyles style; FormatProvider _; result ] -> + makeLibCall com r t i "Decimal" "tryParseWithStyle" [ input; style; result ] + |> Some + | "TryParse", _, [ input; FormatProvider _; result ] -> + makeLibCall com r t i "Decimal" "tryParse" [ input; result ] |> Some + | "TryParse", _, [ input; ExprTypeEnum Types.numberStyles style; result ] -> + makeLibCall com r t i "Decimal" "tryParseWithStyle" [ input; style; result ] + |> Some + | "TryParse", _, [ input; result ] -> makeLibCall com r t i "Decimal" "tryParse" args |> Some | "Parse", _, _ -> makeLibCall com r t i "Decimal" "parse" args |> Some | "TryParse", _, _ -> makeLibCall com r t i "Decimal" "tryParse" args |> Some | Patterns.SetContains Operators.compareSet, _, [ left; right ] -> @@ -2165,6 +2163,9 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: makeLibCall com r t i "Decimal" meth args |> Some | ("CopySign" | "FromOACurrency" | "GetTypeCode" | "ToOACurrency") as meth, _, _ -> makeLibCall com r t i "Decimal" (Naming.lowerFirst meth) args |> Some + | ("ToByte" | "ToSByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64" | "ToSingle" | "ToDouble"), + None, + [ _ ] -> convertTo com ctx r t args |> Some | ("get_Zero" | "get_One" | "get_MinusOne" | "get_MinValue" | "get_MaxValue"), _, _ -> libValue com r t "Decimal" (Naming.removeGetSetPrefix i.CompiledName) [] |> Some | ("IsInteger" | "IsEvenInteger" | "IsOddInteger" | "IsCanonical" | "IsNegative" | "IsPositive"), _, _ -> @@ -2174,22 +2175,16 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: | "Round", _, _ -> match args with | [ x ] -> makeLibCall com r t i "Decimal" "round" args |> Some + | [ x; ExprTypeEnum "System.MidpointRounding" mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some | [ x; ExprTypeAs(Number(Int32, _), dp) ] -> makeLibCall com r t i "Decimal" "roundTo" args |> Some | [ x; mode ] -> makeLibCall com r t i "Decimal" "roundMode" args |> Some | [ x; dp; mode ] -> makeLibCall com r t i "Decimal" "roundToMode" args |> Some | _ -> None - // | "ToString", [StringConst fmt] -> - // let rustFmt = fmt // TODO: replace format specifiers with proper Rust format - // let format = makeStrConst ("{0:" + rustFmt + "}") - // "sprintf!" |> emitFormat com r t [format; thisArg] |> Some - | "ToString", Some c, [] -> - // For d.ToString(provider) the provider is the only argument, so dropping - // it leaves nothing for toString's decimal parameter; the receiver takes - // its place. - let args = [ c ] - - makeLibCall com r t i "Decimal" "toString" args |> Some - | "ToString", _, _ -> makeLibCall com r t i "Decimal" "toString" args |> Some + | "ToString", Some c, [ format; FormatProvider _ ] -> + libCall com r t "Decimal" "toStringWithFormat" [ c; format ] |> Some + | "ToString", Some c, [ FormatProvider _ ] -> makeLibCall com r t i "Decimal" "toString" [ c ] |> Some + | "ToString", Some c, [ format ] -> libCall com r t "Decimal" "toStringWithFormat" [ c; format ] |> Some + | "ToString", Some c, [] -> makeLibCall com r t i "Decimal" "toString" [ c ] |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), thisArg, _ -> valueTypes com ctx r t i thisArg args | _ -> None @@ -2241,9 +2236,9 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr [ arg ] -> arg |> Some | "EnumOfValue", [ arg ] -> TypeCast(arg, t) |> Some | "EnumToValue", [ arg ] -> TypeCast(arg, t) |> Some - | ("GenericHash" | "GenericHashIntrinsic"), [ arg ] -> getHashCode com ctx r arg |> Some - | ("FastHashTuple2" | "FastHashTuple3" | "FastHashTuple4" | "FastHashTuple5" | "GenericHashWithComparer" | "GenericHashWithComparerIntrinsic"), - [ comp; arg ] -> makeInstanceCall r t i comp "GetHashCode" [ arg ] |> Some + | ("GenericHash" | "GenericHashIntrinsic"), [ arg ] -> getHashCode com ctx r t arg |> Some + // | ("FastHashTuple2" | "FastHashTuple3" | "FastHashTuple4" | "FastHashTuple5" | "GenericHashWithComparer" | "GenericHashWithComparerIntrinsic"), + // [ comp; arg ] -> makeInstanceCall r t i comp "GetHashCode" [ arg ] |> Some | ("GenericComparison" | "GenericComparisonIntrinsic"), [ left; right ] -> compare com ctx r left right |> Some | ("FastCompareTuple2" | "FastCompareTuple3" | "FastCompareTuple4" | "FastCompareTuple5" | "GenericComparisonWithComparer" | "GenericComparisonWithComparerIntrinsic"), [ comp; left; right ] -> makeInstanceCall r t i comp "Compare" [ left; right ] |> Some @@ -2263,7 +2258,7 @@ let languagePrimitives (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr [ comp; left; right ] -> makeInstanceCall r t i comp "Equals" [ left; right ] |> Some | ("PhysicalEquality" | "PhysicalEqualityIntrinsic"), [ left; right ] -> referenceEquals com ctx r left right |> Some - | ("PhysicalHash" | "PhysicalHashIntrinsic"), [ arg ] -> referenceHash com ctx r arg |> Some + | ("PhysicalHash" | "PhysicalHashIntrinsic"), [ arg ] -> referenceHash com ctx r t arg |> Some | ("GenericEqualityComparer" | "GenericEqualityERComparer" | "FastGenericComparer" | "FastGenericComparerFromTable" | "FastGenericEqualityComparer" | "FastGenericEqualityComparerFromTable"), _ -> fsharpModule com ctx r t i thisArg args | ("ParseInt32" | "ParseUInt32" | "ParseInt64" | "ParseUInt64"), [ arg ] -> convertTo com ctx r t args |> Some @@ -2310,7 +2305,7 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr let runtimeHelpers (com: ICompiler) (ctx: Context) r t (i: CallInfo) thisArg args = match i.CompiledName, args with - | "GetHashCode", [ arg ] -> getHashCode com ctx r arg |> Some + | "GetHashCode", [ arg ] -> getHashCode com ctx r t arg |> Some | _ -> None // ExceptionDispatchInfo is used to raise exceptions through different threads in async workflows @@ -2412,7 +2407,7 @@ let exceptions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr let unchecked (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match i.CompiledName, args with | "DefaultOf", _ -> (genArg com ctx r 0 i.GenericArgs) |> getZero com ctx |> Some - | "Hash", [ arg ] -> getHashCode com ctx r arg |> Some + | "Hash", [ arg ] -> getHashCode com ctx r t arg |> Some | "Equals", [ arg1; arg2 ] -> equals com ctx r arg1 arg2 |> Some | "Compare", [ arg1; arg2 ] -> compare com ctx r arg1 arg2 |> Some | "NonNull", [ arg ] -> arg |> Some @@ -2476,6 +2471,9 @@ let convert (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) ( [ ExprType(String); ExprType(Number(Int32, _)) ] -> toRadixInt com ctx r t i args |> Some | ("ToSByte" | "ToByte" | "ToInt16" | "ToUInt16" | "ToInt32" | "ToUInt32" | "ToInt64" | "ToUInt64"), [ arg ] -> toRoundInt com ctx r t i args |> Some + | "ToDecimal", [ ExprTypeAs(String, input) ] -> libCall com r t "Decimal" "convert" [ input ] |> Some + | "ToDecimal", [ ExprTypeAs(String, input); FormatProvider _ ] -> + libCall com r t "Decimal" "convert" [ input ] |> Some | ("ToSingle" | "ToDouble" | "ToDecimal"), [ arg ] -> convertTo com ctx r t args |> Some | "ToChar", [ arg ] -> convertTo com ctx r t args |> Some | "ToString", [ arg ] -> toString com ctx r args |> Some @@ -2562,8 +2560,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2571,8 +2568,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_milli_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_milli_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2581,8 +2577,7 @@ let dateTimes (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) - ExprType(Number(_, NumberInfo.IsEnum ent)) ] when ent.FullName = "System.DateTimeKind" -> - "new_ymdhms_micro_kind" |> Some + ExprTypeEnum Types.datetimeKind _ ] -> "new_ymdhms_micro_kind" |> Some | [ ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) ExprType(Number(Int32, _)) @@ -2851,19 +2846,9 @@ let files (com: ICompiler) (ctx: Context) r t (i: CallInfo) (_: Expr option) (ar // Initial support, making at least InvariantCulture compile-able // to be used System.Double.Parse and System.Single.Parse // see https://github.com/fable-compiler/Fable/pull/1197#issuecomment-348034660 -let globalization - (com: ICompiler) - (ctx: Context) - (_: SourceLocation option) - t - (i: CallInfo) - (_: Expr option) - (_: Expr list) - = +let globalization (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName with - | "get_InvariantCulture" -> - // System.Globalization namespace is not supported by Fable. The value InvariantCulture will be compiled to an empty object literal - ObjectExpr([], t, None) |> Some + | "get_InvariantCulture" -> bclType com ctx r t i thisArg args | _ -> None let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -2879,7 +2864,7 @@ let random (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opti else meth |> Naming.lowerFirst - Helper.InstanceCall(c, meth, t, args, i.SignatureArgTypes, ?loc = r) |> Some + makeInstanceCall r t i c meth args |> Some | _ -> None let cancels (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index 73d06ab41b..58ed2763b4 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -225,6 +225,9 @@ module Types = [] let datetimeOffset = "System.DateTimeOffset" + [] + let datetimeKind = "System.DateTimeKind" + [] let dateOnly = "System.DateOnly" @@ -237,6 +240,12 @@ module Types = [] let cultureInfo = "System.Globalization.CultureInfo" + [] + let numberStyles = "System.Globalization.NumberStyles" + + [] + let stringSplitOptions = "System.StringSplitOptions" + [] let timer = "System.Timers.Timer" @@ -756,6 +765,25 @@ module AST = let inline (|EntFullName|) (e: Entity) = e.FullName let inline (|EntRefFullName|) (e: EntityRef) = e.FullName + [] + let (|ExprTypeEnum|_|) (fullName: string) (expr: Expr) = + match expr.Type with + | Number(_, NumberInfo.IsEnum entRef) when entRef.FullName = fullName -> ValueSome expr + | _ -> ValueNone + + [] + let (|ExprTypeObj|_|) (fullName: string) (expr: Expr) = + match expr.Type with + | DeclaredType(entRef, _) when entRef.FullName = fullName -> ValueSome expr + | _ -> ValueNone + + [] + let (|FormatProvider|_|) (expr: Expr) = + match expr with + | ExprTypeObj Types.cultureInfo _ + | ExprTypeObj "System.IFormatProvider" _ -> ValueSome expr + | _ -> ValueNone + [] let (|DeclaredTypeFullName|_|) = function diff --git a/src/fable-library-rust/Cargo.toml b/src/fable-library-rust/Cargo.toml index e36eed581c..67c7a7578a 100644 --- a/src/fable-library-rust/Cargo.toml +++ b/src/fable-library-rust/Cargo.toml @@ -32,7 +32,7 @@ num-integer = { version = "0.1", optional = true } num-traits = { version = "0.2", optional = true } rand = { version = "0.10", features = ["std_rng"], default-features = false, optional = true } regex = { version = "1.13", optional = true } -rust_decimal = { version = "1.42", features = ["maths"], default-features = false, optional = true } +rust_decimal = { version = "1.43", features = ["maths"], default-features = false, optional = true } startup = { version = "0.1", path = "vendored/startup", optional = true } unicode-general-category = { version = "1.1", optional = true } diff --git a/src/fable-library-rust/src/BigInt.rs b/src/fable-library-rust/src/BigInt.rs index 871f4828b5..8f3ed81113 100644 --- a/src/fable-library-rust/src/BigInt.rs +++ b/src/fable-library-rust/src/BigInt.rs @@ -2,7 +2,7 @@ pub mod BigInt_ { use crate::Decimal_::{decimal, truncate}; use crate::NativeArray_::{array_from, Array}; - use crate::Native_::{compare, getHashCode, DivideByInt, Hashable, Lrc, MutCell, ToString, Vec}; + use crate::Native_::{compare, hash_code, DivideByInt, Hashable, Lrc, MutCell, ToString, Vec}; use crate::String_::{string, toString as toString_1}; use num_bigint::*; @@ -43,7 +43,7 @@ pub mod BigInt_ { impl Hashable for bigint { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } @@ -103,7 +103,8 @@ pub mod BigInt_ { pub fn one() -> bigint { BigInt::one().into() } pub fn minusOne() -> bigint { (-BigInt::one()).into() } - // pub fn getHashCode(x: bigint) = x.GetHashCode() + // pub fn getHashCode(x: bigint) = hash_code(&x) + pub fn equals(x: bigint, y: bigint) -> bool { x.eq(&y) } pub fn compareTo(x: bigint, y: bigint) -> i32 { compare(&x, &y) } diff --git a/src/fable-library-rust/src/DateOnly.rs b/src/fable-library-rust/src/DateOnly.rs index 1e028bbbb2..057d6ccd8e 100644 --- a/src/fable-library-rust/src/DateOnly.rs +++ b/src/fable-library-rust/src/DateOnly.rs @@ -3,7 +3,7 @@ pub mod DateOnly_ { use crate::{ DateTime_::{DateTime, DateTimeKind, ticks_to_duration}, Format::DateTime::{format_dotnet_custom, try_parse_date_str}, - Native_::{Hashable, MutCell, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::ticks_per_day, @@ -23,7 +23,7 @@ pub mod DateOnly_ { impl Hashable for DateOnly { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } diff --git a/src/fable-library-rust/src/DateTime.rs b/src/fable-library-rust/src/DateTime.rs index c736d73443..62e884a1df 100644 --- a/src/fable-library-rust/src/DateTime.rs +++ b/src/fable-library-rust/src/DateTime.rs @@ -6,7 +6,7 @@ pub mod DateTime_ { format_dotnet_custom, format_offset_token, format_roundtrip_datetime, try_parse_datetime_str, }, - Native_::{Hashable, MutCell, String, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, String, ToString, compare, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::{TimeSpan, nanoseconds_per_tick, ticks_per_second}, @@ -64,6 +64,7 @@ pub mod DateTime_ { } impl Hash for DateTime { + #[inline] fn hash(&self, state: &mut H) { self.ticks().hash(state) } @@ -72,7 +73,7 @@ pub mod DateTime_ { impl Hashable for DateTime { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.ticks()) + hash_code(&self.ticks()) } } diff --git a/src/fable-library-rust/src/DateTimeOffset.rs b/src/fable-library-rust/src/DateTimeOffset.rs index a1e5f9d112..23be6e963e 100644 --- a/src/fable-library-rust/src/DateTimeOffset.rs +++ b/src/fable-library-rust/src/DateTimeOffset.rs @@ -6,7 +6,7 @@ pub mod DateTimeOffset_ { Format::DateTime::{ format_dotnet_custom, format_roundtrip_datetime, try_parse_datetime_offset_str, }, - Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, format, hash_code}, String_::{fromString, string}, TimeOnly_::TimeOnly, TimeSpan_::{ @@ -57,6 +57,7 @@ pub mod DateTimeOffset_ { } impl Hash for DateTimeOffset { + #[inline] fn hash(&self, state: &mut H) { self.utcTicks().hash(state) } @@ -65,7 +66,7 @@ pub mod DateTimeOffset_ { impl Hashable for DateTimeOffset { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.utcTicks()) + hash_code(&self.utcTicks()) } } diff --git a/src/fable-library-rust/src/Decimal.rs b/src/fable-library-rust/src/Decimal.rs index 60f521b6a4..82c5434e39 100644 --- a/src/fable-library-rust/src/Decimal.rs +++ b/src/fable-library-rust/src/Decimal.rs @@ -1,8 +1,8 @@ #[cfg(feature = "decimal")] pub mod Decimal_ { - use crate::Native_::{compare, getHashCode, DivideByInt, Hashable, Lrc, MutCell, Vec}; + use crate::Native_::{compare, format, hash_code, DivideByInt, Hashable, Lrc, MutCell, NullableRef, String}; use crate::NativeArray_::{new_array, Array}; - use crate::String_::{string, toString as toString_1}; + use crate::String_::{fromString as fromString_1, string, toString as toString_1}; use core::cmp::Ordering; use rust_decimal::prelude::*; @@ -24,10 +24,12 @@ pub mod Decimal_ { impl Hashable for decimal { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } + // pub fn getHashCode(x: decimal) -> i32 { hash_code(&x) } + pub fn equals(x: decimal, y: decimal) -> bool { x.eq(&y) } pub fn compareTo(x: decimal, y: decimal) -> i32 { compare(&x, &y) } @@ -40,11 +42,23 @@ pub mod Decimal_ { if x.is_sign_negative() == sign.is_sign_negative() { x } else { -x } } - pub fn max(x: decimal, y: decimal) -> decimal { x.max(y) } - pub fn min(x: decimal, y: decimal) -> decimal { x.min(y) } + pub fn max(x: decimal, y: decimal) -> decimal { if x < y { y } else { x } } + pub fn min(x: decimal, y: decimal) -> decimal { if x < y { x } else { y } } - pub fn maxMagnitude(x: decimal, y: decimal) -> decimal { if abs(x) > abs(y) {x} else {y} } - pub fn minMagnitude(x: decimal, y: decimal) -> decimal { if abs(x) < abs(y) {x} else {y} } + pub fn maxMagnitude(x: decimal, y: decimal) -> decimal { + match abs(x).cmp(&abs(y)) { + Ordering::Less => y, + Ordering::Greater => x, + Ordering::Equal => if x.is_sign_negative() { y } else { x }, + } + } + pub fn minMagnitude(x: decimal, y: decimal) -> decimal { + match abs(x).cmp(&abs(y)) { + Ordering::Less => x, + Ordering::Greater => y, + Ordering::Equal => if x.is_sign_negative() { x } else { y }, + } + } pub fn clamp(x: decimal, min: decimal, max: decimal) -> decimal { x.clamp(min, max).into() @@ -55,32 +69,58 @@ pub mod Decimal_ { pub fn truncate(x: decimal) -> decimal { x.trunc() } pub fn pown(x: decimal, n: i32) -> decimal { x.powi(n as i64) } - pub fn add(x: decimal, y: decimal) -> decimal { x + y } - pub fn subtract(x: decimal, y: decimal) -> decimal { x - y } - pub fn multiply(x: decimal, y: decimal) -> decimal { x * y } - pub fn divide(x: decimal, y: decimal) -> decimal { x / y } + #[inline] + fn preserveScale(mut value: decimal, scale: u32) -> decimal { + if value.scale() < scale { + value.rescale(scale); + } + value + } + + pub fn add(x: decimal, y: decimal) -> decimal { + let value = x.checked_add(y).unwrap_or_else(|| panic!("")); + preserveScale(value, x.scale().max(y.scale())) + } + pub fn subtract(x: decimal, y: decimal) -> decimal { + let value = x.checked_sub(y).unwrap_or_else(|| panic!("")); + preserveScale(value, x.scale().max(y.scale())) + } + pub fn multiply(x: decimal, y: decimal) -> decimal { + let value = x.checked_mul(y).unwrap_or_else(|| panic!("")); + preserveScale(value, (x.scale() + y.scale()).min(28)) + } + pub fn divide(x: decimal, y: decimal) -> decimal { + let quotient = x.checked_div(y).unwrap_or_else(|| panic!("")); + if quotient.checked_mul(y) == Some(x) { + let quotient = quotient.normalize(); + let scale = quotient.scale().max(x.scale().saturating_sub(y.scale())); + preserveScale(quotient, scale) + } else { + quotient + } + } pub fn remainder(x: decimal, y: decimal) -> decimal { x % y } pub fn negate(x: decimal) -> decimal { -x } pub fn isNegative(x: decimal) -> bool { x.is_sign_negative() } pub fn isPositive(x: decimal) -> bool { x.is_sign_positive() } - pub fn isInteger(x: decimal) -> bool { x.fract() == Decimal::ZERO } - pub fn isEvenInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) == Decimal::ZERO } - pub fn isOddInteger(x: decimal) -> bool { isInteger(x) && (x % Decimal::from(2)) != Decimal::ZERO } + pub fn isInteger(x: decimal) -> bool { x.scale() == 0 || x.fract() == Decimal::ZERO } + pub fn isEvenInteger(x: decimal) -> bool { isInteger(x) && x.trunc().mantissa() % 2 == 0 } + pub fn isOddInteger(x: decimal) -> bool { isInteger(x) && x.trunc().mantissa() % 2 != 0 } // Canonical means the stored scale is already minimal (no representable trailing // zero digits), e.g. 1.0M and 1.20M are non-canonical, but 1M and 3.14M are. pub fn isCanonical(x: decimal) -> bool { x.scale() == x.normalize().scale() } - pub fn toInt8(x: decimal) -> i8 { x.to_i8().unwrap() } - pub fn toUInt8(x: decimal) -> u8 { x.to_u8().unwrap() } - pub fn toInt16(x: decimal) -> i16 { x.to_i16().unwrap() } - pub fn toUInt16(x: decimal) -> u16 { x.to_u16().unwrap() } - pub fn toInt32(x: decimal) -> i32 { x.to_i32().unwrap() } - pub fn toUInt32(x: decimal) -> u32 { x.to_u32().unwrap() } - pub fn toInt64(x: decimal) -> i64 { x.to_i64().unwrap() } - pub fn toUInt64(x: decimal) -> u64 { x.to_u64().unwrap() } - pub fn toNativeInt(x: decimal) -> isize { x.to_isize().unwrap() } - pub fn toUNativeInt(x: decimal) -> usize { x.to_usize().unwrap() } + pub fn toInt8(x: decimal) -> i8 { x.trunc().to_i8().unwrap() } + pub fn toUInt8(x: decimal) -> u8 { x.trunc().normalize().to_u8().unwrap() } + pub fn toInt16(x: decimal) -> i16 { x.trunc().to_i16().unwrap() } + pub fn toUInt16(x: decimal) -> u16 { x.trunc().normalize().to_u16().unwrap() } + pub fn toInt32(x: decimal) -> i32 { x.trunc().to_i32().unwrap() } + pub fn toUInt32(x: decimal) -> u32 { x.trunc().normalize().to_u32().unwrap() } + pub fn toInt64(x: decimal) -> i64 { x.trunc().to_i64().unwrap() } + pub fn toUInt64(x: decimal) -> u64 { x.trunc().normalize().to_u64().unwrap() } + pub fn toNativeInt(x: decimal) -> isize { x.trunc().to_isize().unwrap() } + pub fn toUNativeInt(x: decimal) -> usize { x.trunc().normalize().to_usize().unwrap() } pub fn toFloat32(x: decimal) -> f32 { x.to_f32().unwrap() } pub fn toFloat64(x: decimal) -> f64 { x.to_f64().unwrap() } @@ -102,25 +142,489 @@ pub mod Decimal_ { } pub fn toString(x: decimal) -> string { - toString_1(&x) + toString_1(&if x.is_zero() { x.abs() } else { x }) + } + + fn exponentialFormatString(x: decimal, precision: usize, lowercase: bool) -> String { + if x.is_zero() { + return format!( + "{:.*}{}+000", + precision, + x.normalize(), + if lowercase { 'e' } else { 'E' } + ); + } + + let mantissa = x.mantissa().unsigned_abs(); + let digitCount = mantissa.ilog10() as usize + 1; + let mut exponent = digitCount as i32 - x.scale() as i32 - 1; + let significantDigits = precision.saturating_add(1); + let rounded = if significantDigits < digitCount { + let divisor = 10u128.pow((digitCount - significantDigits) as u32); + let mut rounded = mantissa / divisor + u128::from(mantissa % divisor >= divisor / 2); + if rounded.ilog10() as usize + 1 > significantDigits { + rounded /= 10; + exponent += 1; + } + rounded + } else { + mantissa + }; + let digits = format!("{rounded}"); + let mut formatted = String::new(); + if x.is_sign_negative() { + formatted.push('-'); + } + formatted.push(digits.as_bytes()[0] as char); + if precision > 0 { + formatted.push('.'); + formatted.push_str(&digits[1..]); + for _ in digits.len()..significantDigits { + formatted.push('0'); + } + } + formatted.push(if lowercase { 'e' } else { 'E' }); + formatted.push(if exponent < 0 { '-' } else { '+' }); + formatted.push_str(&format!("{:03}", exponent.unsigned_abs())); + formatted + } + + fn exponentialFormat(x: decimal, precision: usize, lowercase: bool) -> string { + fromString_1(exponentialFormatString(x, precision, lowercase)) + } + + fn generalFormat(x: decimal, precision: usize, lowercase: bool) -> string { + if x.is_zero() { + return toString_1(&Decimal::ZERO); + } + + let normalized = x.abs().normalize(); + let mantissa = normalized.mantissa() as u128; + let digitCount = mantissa.ilog10() + 1; + let exponent = digitCount as i32 - normalized.scale() as i32 - 1; + let rounded = if exponent < precision as i32 { + let decimalPlaces = (precision as i32 - exponent - 1).clamp(0, 28) as u32; + x.round_dp_with_strategy(decimalPlaces, RoundingStrategy::MidpointAwayFromZero).normalize() + } else { + x.normalize() + }; + let magnitude = rounded.abs(); + let exponent = (magnitude.mantissa() as u128).ilog10() as i32 - magnitude.scale() as i32; + + if exponent >= precision as i32 || exponent < -4 { + let mut formatted = exponentialFormatString(rounded, precision - 1, lowercase); + if let Some(point) = formatted.find('.') { + let exponentIndex = formatted.find(['e', 'E']).unwrap(); + let mut end = exponentIndex; + while end > point + 1 && formatted.as_bytes()[end - 1] == b'0' { + end -= 1; + } + if end == point + 1 { + end = point; + } + formatted.replace_range(end..exponentIndex, ""); + } + let exponentIndex = formatted.find(['e', 'E']).unwrap(); + if formatted.as_bytes().get(exponentIndex + 2) == Some(&b'0') { + formatted.remove(exponentIndex + 2); + } + fromString_1(formatted) + } else { + toString_1(&rounded) + } + } + + pub fn toStringWithFormat(x: decimal, formatSpec: string) -> string { + let formatSpec = formatSpec.as_str(); + let Some(token) = formatSpec.chars().next() else { + return toString(x); + }; + let specifiedPrecision = formatSpec + .get(1..) + .and_then(|value| value.parse::().ok()); + let precision = specifiedPrecision.unwrap_or(0); + + match token { + 'G' | 'g' if precision > 0 => generalFormat(x, precision, token == 'g'), + 'G' | 'g' | 'R' | 'r' => toString(x), + 'E' | 'e' => exponentialFormat(x, specifiedPrecision.unwrap_or(6), token == 'e'), + 'F' | 'f' => { + let precision = specifiedPrecision.unwrap_or(2); + let rounded = x.round_dp_with_strategy(precision.min(28) as u32, RoundingStrategy::MidpointAwayFromZero); + let rounded = if rounded.is_zero() { rounded.abs() } else { rounded }; + fromString_1(format!("{:.*}", precision, rounded)) + }, + _ => toString(x), + } } pub fn getTypeCode() -> i32 { 15 } - pub fn tryParse(s: string, res: &MutCell) -> bool { - match Decimal::from_str(s.trim()) { - Ok(d) => { res.set(d); true }, - Err(e) => false, + // System.Globalization.NumberStyles + #[allow(dead_code)] + mod NumberStyles { + pub const None: i32 = 0x000; + pub const AllowLeadingWhite: i32 = 0x001; + pub const AllowTrailingWhite: i32 = 0x002; + pub const AllowLeadingSign: i32 = 0x004; + pub const AllowTrailingSign: i32 = 0x008; + pub const AllowParentheses: i32 = 0x010; + pub const AllowDecimalPoint: i32 = 0x020; + pub const AllowThousands: i32 = 0x040; + pub const AllowExponent: i32 = 0x080; + pub const AllowCurrencySymbol: i32 = 0x100; + pub const AllowHexSpecifier: i32 = 0x200; + pub const AllowBinarySpecifier: i32 = 0x400; + + pub const Integer: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign; + pub const HexNumber: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowHexSpecifier; + pub const BinaryNumber: i32 = AllowLeadingWhite | AllowTrailingWhite | AllowBinarySpecifier; + pub const Number: i32 = + AllowLeadingWhite + | AllowTrailingWhite + | AllowLeadingSign + | AllowTrailingSign + | AllowDecimalPoint + | AllowThousands; + pub const Float: i32 = + AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowDecimalPoint | AllowExponent; + pub const Currency: i32 = + AllowLeadingWhite + | AllowTrailingWhite + | AllowLeadingSign + | AllowTrailingSign + | AllowParentheses + | AllowDecimalPoint + | AllowThousands + | AllowCurrencySymbol; + pub const Any: i32 = Currency | AllowExponent; + } + + mod parsing { + use super::decimal; + use super::NumberStyles; + use rust_decimal::Decimal; + + #[derive(Clone, Copy, PartialEq, Eq)] + enum DecimalParseError { + Format, + Overflow, + } + + fn parseFailure(error: DecimalParseError, value: &str) -> ! { + match error { + DecimalParseError::Format => panic!("The input string '{}' was not in a correct format.", value), + DecimalParseError::Overflow => panic!("Value was either too large or too small for a Decimal."), + } + } + + pub(super) fn overflowFailure() -> ! { parseFailure(DecimalParseError::Overflow, "") } + + const MAX_MANTISSA: u128 = (1_u128 << 96) - 1; + const SIGNIFICANT_WINDOW: usize = 30; + const EXPONENT_LIMIT: i128 = 1_i128 << 62; + + // The significant digits of a number, without leading zeros. Parsing never needs more than the first + // SIGNIFICANT_WINDOW of them: a mantissa has at most 29 digits and one more is needed for rounding. + // `hasTail` records whether any later digit is non-zero, which is all the rounding needs from them. + struct SignificantDigits { + window: [u8; SIGNIFICANT_WINDOW], + length: usize, + hasTail: bool, + } + + impl SignificantDigits { + fn push(&mut self, digit: u8) { + if self.length < SIGNIFICANT_WINDOW { + self.window[self.length] = digit; + } else if digit != 0 { + self.hasTail = true; + } + self.length += 1; + } + + // Index 0 is the first significant digit. Negative indexes are the zeros between the decimal point + // and the first significant digit. + fn digitAt(&self, index: i128) -> u128 { + if index < 0 { + 0 + } else { + u128::from(self.window[index as usize]) + } + } + + // `start` is never past SIGNIFICANT_WINDOW, so every digit recorded in `hasTail` is at or after it. + fn hasNonZeroFrom(&self, start: usize) -> bool { + let stored = self.length.min(SIGNIFICANT_WINDOW); + self.hasTail || self.window[start.min(stored)..stored].iter().any(|digit| *digit != 0) + } + } + + // Reads the mantissa and the exponent in a single pass. Returns the significant digits and the power of + // ten of their last digit, so the value is digits * 10^power. + fn scanNumber( + value: &str, + allowDecimalPoint: bool, + allowThousands: bool, + allowExponent: bool, + ) -> Result<(SignificantDigits, i128), DecimalParseError> { + let bytes = value.as_bytes(); + let mut digits = SignificantDigits { + window: [0; SIGNIFICANT_WINDOW], + length: 0, + hasTail: false, + }; + let mut digitCount = 0_usize; + let mut fractionLength = 0_i128; + let mut hasDecimalPoint = false; + let mut hasIntegerDigit = false; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + byte @ b'0'..=b'9' => { + digitCount += 1; + if hasDecimalPoint { + fractionLength += 1; + } else { + hasIntegerDigit = true; + } + if digits.length > 0 || byte != b'0' { + digits.push(byte - b'0'); + } + } + b'.' if allowDecimalPoint && !hasDecimalPoint => hasDecimalPoint = true, + b',' if allowThousands && !hasDecimalPoint && hasIntegerDigit => {} + b'e' | b'E' => break, + _ => return Err(DecimalParseError::Format), + } + index += 1; + } + if digitCount == 0 { + return Err(DecimalParseError::Format); + } + + let exponent = if index < bytes.len() { + if !allowExponent { + return Err(DecimalParseError::Format); + } + parseExponent(&bytes[index + 1..])? + } else { + 0 + }; + Ok((digits, exponent - fractionLength)) + } + + // Parses the text after the exponent marker. Magnitudes are clamped to EXPONENT_LIMIT so the arithmetic + // cannot overflow; any exponent beyond the clamp gives the same result. + fn parseExponent(text: &[u8]) -> Result { + let (isNegative, digits) = match text.first() { + Some(b'-') => (true, &text[1..]), + Some(b'+') => (false, &text[1..]), + _ => (false, text), + }; + if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) { + return Err(DecimalParseError::Format); + } + let magnitude = digits.iter().fold(0_i128, |magnitude, digit| { + (magnitude * 10 + i128::from(*digit - b'0')).min(EXPONENT_LIMIT) + }); + Ok(if isNegative { -magnitude } else { magnitude }) + } + + fn fromMantissa(mantissa: u128, scale: u32, isNegative: bool) -> decimal { + let mut parsed = Decimal::from_parts( + mantissa as u32, + (mantissa >> 32) as u32, + (mantissa >> 64) as u32, + isNegative, + scale, + ); + parsed.set_sign_negative(isNegative); + parsed + } + + // Rounds digits * 10^power to a Decimal with at most 28 fraction digits, rounding half to even. + fn roundDigits(digits: &SignificantDigits, power: i128, isNegative: bool) -> Result { + if digits.length == 0 { + return Ok(fromMantissa(0, (-power).clamp(0, 28) as u32, isNegative)); + } + // Common case: every digit fits the mantissa and the scale, so nothing is rounded. + if digits.length <= 28 && (-28..=0).contains(&power) { + let mantissa = digits.window[..digits.length] + .iter() + .fold(0_u128, |mantissa, digit| mantissa * 10 + u128::from(*digit)); + return Ok(fromMantissa(mantissa, (-power) as u32, isNegative)); + } + let position = digits.length as i128 + power; + if position > 29 { + return Err(DecimalParseError::Overflow); + } + if position < -28 { + return Ok(fromMantissa(0, 28, isNegative)); + } + // The mantissa takes the digits up to the decimal point (zeros past the last digit when power >= 0), + // then as many fraction digits as still fit. + let fractionLength = -power; + let mut mantissa = 0_u128; + if position > 0 { + for index in 0..position { + mantissa = mantissa * 10 + digits.digitAt(index); + } + if mantissa > MAX_MANTISSA { + return Err(DecimalParseError::Overflow); + } + } + let mut scale = 0_i128; + while scale < fractionLength.min(28) { + let next = mantissa * 10 + digits.digitAt(position + scale); + if next > MAX_MANTISSA { + break; + } + mantissa = next; + scale += 1; + } + + let roundUp = scale < fractionLength + && match digits.digitAt(position + scale) { + 0..=4 => false, + 5 => mantissa % 2 == 1 || digits.hasNonZeroFrom((position + scale + 1) as usize), + _ => true, + }; + if !roundUp || mantissa < MAX_MANTISSA { + return Ok(fromMantissa(mantissa + u128::from(roundUp), scale as u32, isNegative)); + } + if scale == 0 { + return Err(DecimalParseError::Overflow); + } + // Rounding up would exceed MAX_MANTISSA, so the result keeps one fraction digit fewer. + Ok(fromMantissa(MAX_MANTISSA / 10 + 1, scale as u32 - 1, isNegative)) + } + + // Space and U+0009..U+000D, the whitespace .NET accepts around a number. + fn isParseWhitespace(c: char) -> bool { matches!(c, ' ' | '\t'..='\r') } + + pub(super) fn validateStyle(style: i32) { + if style & !NumberStyles::Any != 0 { + panic!("An undefined NumberStyles value is being used."); + } + } + + fn parseDecimalWithStyle(value: &str, style: i32) -> Result { + validateStyle(style); + let has = |flag: i32| style & flag != 0; + let allowLeadingWhite = has(NumberStyles::AllowLeadingWhite); + let allowTrailingWhite = has(NumberStyles::AllowTrailingWhite); + let allowLeadingSign = has(NumberStyles::AllowLeadingSign); + let allowTrailingSign = has(NumberStyles::AllowTrailingSign); + let allowParentheses = has(NumberStyles::AllowParentheses); + let allowCurrencySymbol = has(NumberStyles::AllowCurrencySymbol); + + // Disallowed whitespace is left in place and rejected by scanNumber. + let mut value = value.trim_end_matches('\0'); + + let mut isNegative = false; + let mut hasSign = false; + let mut hasParentheses = false; + let mut hasCurrencySymbol = false; + loop { + if allowLeadingWhite && (!hasSign || hasCurrencySymbol) { + value = value.trim_start_matches(isParseWhitespace); + } + if allowLeadingSign && !hasSign && matches!(value.as_bytes().first(), Some(b'+' | b'-')) { + isNegative = value.starts_with('-'); + hasSign = true; + value = &value[1..]; + } else if allowParentheses && !hasSign && value.starts_with('(') { + isNegative = true; + hasSign = true; + hasParentheses = true; + value = &value[1..]; + } else if allowCurrencySymbol && !hasCurrencySymbol && value.starts_with('\u{00a4}') { + hasCurrencySymbol = true; + value = &value['\u{00a4}'.len_utf8()..]; + } else { + break; + } + } + + loop { + if allowTrailingWhite { + value = value.trim_end_matches(isParseWhitespace); + } + if allowTrailingSign && !hasSign && matches!(value.as_bytes().last(), Some(b'+' | b'-')) { + isNegative = value.ends_with('-'); + hasSign = true; + value = &value[..value.len() - 1]; + } else if hasParentheses && value.ends_with(')') { + hasParentheses = false; + value = &value[..value.len() - 1]; + } else if allowCurrencySymbol && !hasCurrencySymbol && value.ends_with('\u{00a4}') { + hasCurrencySymbol = true; + value = &value[..value.len() - '\u{00a4}'.len_utf8()]; + } else { + break; + } + } + if hasParentheses { + return Err(DecimalParseError::Format); + } + + let (digits, power) = scanNumber( + value, + has(NumberStyles::AllowDecimalPoint), + has(NumberStyles::AllowThousands), + has(NumberStyles::AllowExponent), + )?; + roundDigits(&digits, power, isNegative) + } + + pub(super) fn tryParseDecimal(value: &str, style: i32) -> Option { + parseDecimalWithStyle(value, style).ok() + } + + pub(super) fn parseDecimal(value: &str, style: i32) -> decimal { + match parseDecimalWithStyle(value, style) { + Ok(d) => d, + Err(error) => parseFailure(error, value), + } + } + + // Parses the text produced by float formatting and drops insignificant trailing zeros. + pub(super) fn fromFloatString(value: &str) -> decimal { + let parsed = parseDecimal(value, NumberStyles::Float); + if parsed.is_zero() { Decimal::ZERO } else { parsed.normalize() } } } - pub fn parse(s: string) -> decimal { - match Decimal::from_str(s.trim()) { - Ok(d) => d, - Err(e) => panic!("The input string {} was not in a correct format.", s), + pub fn tryParseWithStyle(s: string, style: i32, res: &MutCell) -> bool { + if s.is_null() { + parsing::validateStyle(style); + res.set(Decimal::ZERO); + return false; + } + match parsing::tryParseDecimal(&s, style) { + Some(d) => { res.set(d); true }, + None => { res.set(Decimal::ZERO); false }, } } + pub fn tryParse(s: string, res: &MutCell) -> bool { + tryParseWithStyle(s, NumberStyles::Number, res) + } + + pub fn parseWithStyle(s: string, style: i32) -> decimal { + if s.is_null() { panic!("Value cannot be null."); } + parsing::parseDecimal(&s, style) + } + + pub fn parse(s: string) -> decimal { + parseWithStyle(s, NumberStyles::Number) + } + + pub fn convert(s: string) -> decimal { + if s.is_null() { Decimal::ZERO } else { parse(s) } + } + pub fn fromInt8(n: i8) -> decimal { Decimal::from_i8(n).unwrap() } pub fn fromUInt8(n: u8) -> decimal { Decimal::from_u8(n).unwrap() } pub fn fromInt16(n: i16) -> decimal { Decimal::from_i16(n).unwrap() } @@ -132,8 +636,19 @@ pub mod Decimal_ { pub fn fromNativeInt(n: isize) -> decimal { Decimal::from_isize(n).unwrap() } pub fn fromUNativeInt(n: usize) -> decimal { Decimal::from_usize(n).unwrap() } - pub fn fromFloat32(n: f32) -> decimal { Decimal::from_f32(n).unwrap() } - pub fn fromFloat64(n: f64) -> decimal { Decimal::from_f64(n).unwrap() } + pub fn fromFloat32(n: f32) -> decimal { + if !n.is_finite() { + parsing::overflowFailure(); + } + parsing::fromFloatString(&format!("{:.6e}", n)) + } + + pub fn fromFloat64(n: f64) -> decimal { + if !n.is_finite() { + parsing::overflowFailure(); + } + parsing::fromFloatString(&format!("{:.14e}", n)) + } pub fn fromDecimal(d: decimal) -> decimal { d } @@ -149,21 +664,27 @@ pub mod Decimal_ { Decimal::from_u32(c as u32).unwrap() } + // F# `decimal` on strings uses NumberStyles.Float by default. pub fn fromString(s: string) -> decimal { - Decimal::from_str(s.trim()).unwrap() + parseWithStyle(s, NumberStyles::Float) } pub fn fromParts(low: i32, mid: i32, high: i32, isNegative: bool, scale: u8) -> decimal { - Decimal::from_parts(low as u32, mid as u32, high as u32, isNegative, scale as u32) + assert!(scale <= 28, "Invalid decimal scale"); + let mut value = Decimal::from_parts(low as u32, mid as u32, high as u32, isNegative, scale as u32); + value.set_sign_negative(isNegative); + value } pub fn fromInts(low: i32, mid: i32, high: i32, signExp: i32) -> decimal { + assert!(signExp as u32 & 0x7F00_FFFF == 0, "Invalid decimal flags"); let isNegative = signExp < 0; - let scale = ((signExp >> 16) & 0x7F) as u8; + let scale = ((signExp >> 16) & 0xFF) as u8; fromParts(low, mid, high, isNegative, scale) } pub fn fromIntArray(bits: Array) -> decimal { + assert!(bits.len() == 4, "Invalid decimal bits length"); fromInts(bits[0], bits[1], bits[2], bits[3]) } @@ -173,38 +694,39 @@ pub mod Decimal_ { let mid = du.mid as i32; let high = du.hi as i32; let scale = du.scale as i32; - let signExp = - if du.negative { -(scale << 16) } else { scale << 16 }; + let signExp = (scale << 16) | if du.negative { i32::MIN } else { 0 }; new_array(&[low, mid, high, signExp]) } pub fn round(x: decimal) -> decimal { x.round() } pub fn roundTo(x: decimal, dp: i32) -> decimal { + assert!((0..=28).contains(&dp), "Invalid decimal precision"); x.round_dp(dp as u32) } - pub enum MidpointRounding { - ToEven = 0, - AwayFromZero = 1, - ToZero = 2, - ToNegativeInfinity = 3, - ToPositiveInfinity = 4 + pub mod MidpointRounding { + pub const ToEven: i32 = 0; + pub const AwayFromZero: i32 = 1; + pub const ToZero: i32 = 2; + pub const ToNegativeInfinity: i32 = 3; + pub const ToPositiveInfinity: i32 = 4; } - pub fn roundToMode(x: decimal, dp: i32, mode: MidpointRounding) -> decimal { + pub fn roundToMode(x: decimal, dp: i32, mode: i32) -> decimal { + assert!((0..=28).contains(&dp), "Invalid decimal precision"); let strategy = match mode { MidpointRounding::ToEven => RoundingStrategy::MidpointNearestEven, MidpointRounding::AwayFromZero => RoundingStrategy::MidpointAwayFromZero, MidpointRounding::ToZero => RoundingStrategy::ToZero, MidpointRounding::ToNegativeInfinity => RoundingStrategy::ToNegativeInfinity, MidpointRounding::ToPositiveInfinity => RoundingStrategy::ToPositiveInfinity, - // _ => RoundingStrategy::MidpointNearestEven, + _ => panic!("Invalid decimal rounding mode"), }; x.round_dp_with_strategy(dp as u32, strategy) } - pub fn roundMode(x: decimal, mode: MidpointRounding) -> decimal { + pub fn roundMode(x: decimal, mode: i32) -> decimal { roundToMode(x, 0, mode) } diff --git a/src/fable-library-rust/src/Fable.Library.Rust.fsproj b/src/fable-library-rust/src/Fable.Library.Rust.fsproj index 4b2c06b55f..42644799ea 100644 --- a/src/fable-library-rust/src/Fable.Library.Rust.fsproj +++ b/src/fable-library-rust/src/Fable.Library.Rust.fsproj @@ -25,6 +25,7 @@ + diff --git a/src/fable-library-rust/src/Guid.rs b/src/fable-library-rust/src/Guid.rs index 87b97f0ee3..a631589bd4 100644 --- a/src/fable-library-rust/src/Guid.rs +++ b/src/fable-library-rust/src/Guid.rs @@ -1,7 +1,7 @@ #[cfg(feature = "guid")] pub mod Guid_ { use crate::DateTimeOffset_::DateTimeOffset; - use crate::Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}; + use crate::Native_::{Hashable, MutCell, ToString, compare, format, hash_code}; use crate::NativeArray_::{Array, new_array}; use crate::String_::{fromString, string}; use uuid::{NoContext, Timestamp, Uuid}; @@ -22,7 +22,7 @@ pub mod Guid_ { #[inline] fn getHashCode(&self) -> i32 { let bytes = self.toByteArray(); - getHashCode(&bytes) + hash_code(&bytes) } } diff --git a/src/fable-library-rust/src/Interop.rs b/src/fable-library-rust/src/Interop.rs index ac21351720..3a9bd22bca 100644 --- a/src/fable-library-rust/src/Interop.rs +++ b/src/fable-library-rust/src/Interop.rs @@ -128,6 +128,7 @@ pub mod SetExt { } impl Hash for Set { + #[inline] fn hash(&self, state: &mut H) { self.clone().into_iter().for_each(|x| x.hash(state)) } @@ -231,6 +232,7 @@ pub mod MapExt { } impl Hash for Map { + #[inline] fn hash(&self, state: &mut H) { self.clone().into_iter().for_each(|kvp| kvp.hash(state)) } diff --git a/src/fable-library-rust/src/LrcPtr.rs b/src/fable-library-rust/src/LrcPtr.rs index 397917494d..95debbe676 100644 --- a/src/fable-library-rust/src/LrcPtr.rs +++ b/src/fable-library-rust/src/LrcPtr.rs @@ -51,7 +51,7 @@ impl Clone for LrcPtr { impl Hashable for LrcPtr { #[inline] fn getHashCode(&self) -> i32 { - self.0.as_ref().map_or(0, |value| value.getHashCode()) + self.0.getHashCode() } } diff --git a/src/fable-library-rust/src/Mutable.rs b/src/fable-library-rust/src/Mutable.rs index 7fcbaf1918..08293f9df6 100644 --- a/src/fable-library-rust/src/Mutable.rs +++ b/src/fable-library-rust/src/Mutable.rs @@ -95,6 +95,7 @@ mod NonSyncCell { } impl Hash for MutCell { + #[inline] fn hash(&self, state: &mut H) { self.get().hash(state); } diff --git a/src/fable-library-rust/src/Native.rs b/src/fable-library-rust/src/Native.rs index c3ea70f006..655c1cb9b9 100644 --- a/src/fable-library-rust/src/Native.rs +++ b/src/fable-library-rust/src/Native.rs @@ -258,7 +258,7 @@ pub mod Native_ { fn getHashCode(&self) -> i32 { #[allow(non_snake_case)] let ($($ty,)+) = self; - getHashCodeFromHashCodes([$($ty.getHashCode(),)+]) + appendHashCodes([$($ty.getHashCode(),)+]) } } }; @@ -278,7 +278,7 @@ pub mod Native_ { impl Hashable for $ident { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } }; @@ -372,28 +372,34 @@ pub mod Native_ { ((h >> 32) ^ h) as i32 } - pub fn getHashCode(x: &T) -> i32 { + #[inline] + pub fn hash_code(x: &T) -> i32 { hash_to_i32(|hasher| x.hash(hasher)) } + #[inline] + pub fn getHashCode(x: &T) -> i32 { + x.getHashCode() + } + #[inline] pub fn combineHashCodes(x: i32, y: i32) -> i32 { x.wrapping_shl(1).wrapping_add(y).wrapping_add(631) } - pub fn getHashCodeFromHashCodes(hash_codes: I) -> i32 + pub fn appendHashCodes(hash_codes: I) -> i32 where I: IntoIterator, { hash_to_i32(|hasher| { - for hash_code in hash_codes { - hash_code.hash(hasher); + for code in hash_codes { + code.hash(hasher); } }) } pub fn referenceHash(p: &T) -> i32 { - getHashCode(&(p as *const T)) + hash_code(&(p as *const T)) } pub fn compare(x: T, y: T) -> i32 { diff --git a/src/fable-library-rust/src/String.rs b/src/fable-library-rust/src/String.rs index 1a12bd6a74..ce682d23d8 100644 --- a/src/fable-library-rust/src/String.rs +++ b/src/fable-library-rust/src/String.rs @@ -5,7 +5,7 @@ pub mod String_ { // Strings // ----------------------------------------------------------- - use crate::Native_::{compare, iter_to_seq, getHashCode, seq_to_iter, Func1, Func2, Hashable, NullableRef, Seq, String, ToString, Vec}; + use crate::Native_::{compare, iter_to_seq, hash_code, seq_to_iter, Func1, Func2, Hashable, NullableRef, Seq, String, ToString, Vec}; use crate::NativeArray_::{array_from, Array}; use core::cmp::Ordering; @@ -285,7 +285,7 @@ pub mod String_ { impl Hashable for string { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } diff --git a/src/fable-library-rust/src/System.Globalization.fs b/src/fable-library-rust/src/System.Globalization.fs new file mode 100644 index 0000000000..c92538679c --- /dev/null +++ b/src/fable-library-rust/src/System.Globalization.fs @@ -0,0 +1,6 @@ +namespace System.Globalization + +open Global_ + +type CultureInfo() = + static member InvariantCulture = CultureInfo() diff --git a/src/fable-library-rust/src/TimeOnly.rs b/src/fable-library-rust/src/TimeOnly.rs index 2cc57dcc1c..925e1c7e88 100644 --- a/src/fable-library-rust/src/TimeOnly.rs +++ b/src/fable-library-rust/src/TimeOnly.rs @@ -3,7 +3,7 @@ pub mod TimeOnly_ { use crate::{ DateTime_::{DateTime, duration_to_ticks, ticks_to_duration}, Format::DateTime::{format_dotnet_custom, split_meridiem_suffix, try_parse_time_str}, - Native_::{Hashable, MutCell, ToString, compare, format, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, format, hash_code}, String_::{fromString, string}, TimeSpan_::{TimeSpan, nanoseconds_per_tick, ticks_per_day}, }; @@ -23,7 +23,7 @@ pub mod TimeOnly_ { impl Hashable for TimeOnly { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(self) + hash_code(self) } } diff --git a/src/fable-library-rust/src/TimeSpan.rs b/src/fable-library-rust/src/TimeSpan.rs index 238e211793..cb58cf0102 100644 --- a/src/fable-library-rust/src/TimeSpan.rs +++ b/src/fable-library-rust/src/TimeSpan.rs @@ -1,7 +1,7 @@ pub mod TimeSpan_ { use crate::{ Format::TimeSpan::{format_time_span, try_parse_time_span_str}, - Native_::{Hashable, MutCell, ToString, compare, getHashCode}, + Native_::{Hashable, MutCell, ToString, compare, hash_code}, String_::{fromString, string}, }; use core::ops::{Add, Div, Mul, Sub}; @@ -44,7 +44,7 @@ pub mod TimeSpan_ { impl Hashable for TimeSpan { #[inline] fn getHashCode(&self) -> i32 { - getHashCode(&self.ticks()) + hash_code(&self.ticks()) } } diff --git a/src/fable-library-rust/vendored/hashable/src/lib.rs b/src/fable-library-rust/vendored/hashable/src/lib.rs index 413d88d47b..b0c2143b8f 100644 --- a/src/fable-library-rust/vendored/hashable/src/lib.rs +++ b/src/fable-library-rust/vendored/hashable/src/lib.rs @@ -22,6 +22,7 @@ fn expand(input: DeriveInput) -> syn::Result { Ok(quote! { impl #impl_generics ::core::hash::Hash for #ident #ty_generics #where_clause { + #[inline] fn hash(&self, state: &mut H) { #hash_body } @@ -30,7 +31,7 @@ fn expand(input: DeriveInput) -> syn::Result { impl #impl_generics #runtime::Native_::Hashable for #ident #ty_generics #where_clause { #[inline] fn getHashCode(&self) -> i32 { - #runtime::Native_::getHashCode(self) + #runtime::Native_::hash_code(self) } } }) diff --git a/src/quicktest-rust/src/main.fs b/src/quicktest-rust/src/main.fs index deef3d37b3..555ece187c 100644 --- a/src/quicktest-rust/src/main.fs +++ b/src/quicktest-rust/src/main.fs @@ -1,39 +1,21 @@ open System +open System.Collections.Generic -type RefC(v: int) = - member _.V = v +let genericHash<'T when 'T: equality> (x: 'T) : int = + let cmp = EqualityComparer<'T>.Default + cmp.GetHashCode(x) -let twoSwitchRef (x: RefC option) (b: bool) = - match x with - | Some c when b -> c.V + 1 - | Some c -> c.V - | None -> -1 - -type RefU = - | RA of RefC - | RB of RefC - -let orPatRef (u: RefU) = - match u with - | RA c - | RB c -> c.V +[] +let main argv = + Console.WriteLine("Hello {0}!", "World") -type ObjU = - | OA of obj - | OB of obj + let o1 = [| 1; 2 |] + let o2 = [| 1; 2 |] + let o3 = [| 2; 1 |] + let h1 = genericHash o1 + let h2 = genericHash o2 + let h3 = genericHash o3 -let orPatObj (u: ObjU) : obj = - match u with - | OA o - | OB o -> o + Console.WriteLine(" h1 = {0}\n h2 = {1}\n h3 = {2}", h1, h2, h3) -[] -let main argv = - Console.WriteLine(twoSwitchRef (Some(RefC 5)) true) - Console.WriteLine(twoSwitchRef (Some(RefC 5)) false) - Console.WriteLine(twoSwitchRef None false) - Console.WriteLine(orPatRef (RA(RefC 5))) - Console.WriteLine(orPatRef (RB(RefC 7))) - Console.WriteLine((orPatObj (OA(box 9))) :? int) - Console.WriteLine((orPatObj (OB(box 11))) :? string) 0 diff --git a/tests/Rust/tests/src/ArithmeticTests.fs b/tests/Rust/tests/src/ArithmeticTests.fs index 0debfebb99..c4a867b786 100644 --- a/tests/Rust/tests/src/ArithmeticTests.fs +++ b/tests/Rust/tests/src/ArithmeticTests.fs @@ -1,6 +1,7 @@ module Fable.Tests.ArithmeticTests open System +open System.Globalization open Util.Testing let [] posLiteral = 5 @@ -201,6 +202,9 @@ let ``Decimal.IsEvenInteger works`` () = System.Decimal.IsEvenInteger(-4M) |> equal true System.Decimal.IsEvenInteger(3.5M) |> equal false System.Decimal.IsEvenInteger(-3.5M) |> equal false + System.Decimal.IsEvenInteger(4.00M) |> equal true + System.Decimal.IsEvenInteger(-3.00M) |> equal false + System.Decimal.IsEvenInteger(Decimal.MaxValue) |> equal false [] let ``Decimal.IsOddInteger works`` () = @@ -210,6 +214,9 @@ let ``Decimal.IsOddInteger works`` () = System.Decimal.IsOddInteger(-4M) |> equal false System.Decimal.IsOddInteger(3.5M) |> equal false System.Decimal.IsOddInteger(-3.5M) |> equal false + System.Decimal.IsOddInteger(-3.00M) |> equal true + System.Decimal.IsOddInteger(4.00M) |> equal false + System.Decimal.IsOddInteger(Decimal.MinValue) |> equal true [] let ``Decimal.IsNegative works`` () = @@ -394,6 +401,40 @@ let ``Decimal GetBits works`` () = d2 |> equal d d3 |> equal -d +[] +let ``Decimal bits preserve negative sign and scale`` () = + for scale in [0uy; 1uy; 28uy] do + let value = Decimal(123, 0, 0, true, scale) + let bits = Decimal.GetBits(value) + bits[3] |> equal (Int32.MinValue ||| (int scale <<< 16)) + Decimal.GetBits(Decimal(bits)) |> equal bits + throwsAnyError (fun () -> Decimal([| 0; 0; 0; 1 |]) |> ignore) + throwsAnyError (fun () -> Decimal(0, 0, 0, false, 29uy) |> ignore) + Decimal.GetBits(Decimal.Negate(0.00M))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.GetBits(Decimal(0, 0, 0, true, 2uy))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.GetBits(Decimal([| 0; 0; 0; Int32.MinValue |]))[3] |> equal Int32.MinValue + throwsAnyError (fun () -> Decimal.Round(1M, -1) |> ignore) + throwsAnyError (fun () -> Decimal.Round(1M, 29, MidpointRounding.ToEven) |> ignore) + throwsAnyError (fun () -> Decimal.Round(1M, 0, enum 5) |> ignore) + Decimal.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3M + Math.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3M + Decimal.Round(-2.5M, 0, MidpointRounding.ToNegativeInfinity) |> equal -3M + +[] +let ``Decimal formatting rounds significant digits`` () = + let inv = System.Globalization.CultureInfo.InvariantCulture + (2M).ToString("F0", inv) |> equal "2" + (2M).ToString("E0", inv) |> equal "2E+000" + (2.5M).ToString("F0", inv) |> equal "3" + (2.5M).ToString("E0", inv) |> equal "3E+000" + (-2.5M).ToString("E0", inv) |> equal "-3E+000" + (9.99M).ToString("G1", inv) |> equal "1E+01" + (999.9M).ToString("G3", inv) |> equal "1E+03" + (0.00009999M).ToString("G3", inv) |> equal "0.0001" + (100000M).ToString("G3", inv) |> equal "1E+05" + (2500M).ToString("G1", inv) |> equal "3E+03" + Decimal.MaxValue.ToString("E0", inv) |> equal "8E+028" + [] let ``Decimal abs works`` () = abs -4M |> equal 4M @@ -481,6 +522,344 @@ let ``Decimal floor works`` () = Math.Floor 11.25M |> equal 11.M Math.Floor -11.25M |> equal -12.M +[] +let ``Decimal rounding works for values below 0.1`` () = + Math.Floor (decimal -0.0836) |> equal -1.M + Math.Floor (decimal (sin (-5. * Math.PI / 180.))) |> equal -1.M + Math.Floor (decimal "-0.0836") |> equal -1.M + Math.Floor (decimal "-0.0000123") |> equal -1.M + Math.Floor -0.0836M |> equal -1.M + Math.Floor (decimal 0.0836) |> equal 0.M + Math.Ceiling (decimal 0.0836) |> equal 1.M + Math.Ceiling (decimal "0.0000123") |> equal 1.M + Math.Ceiling (decimal -0.0836) |> equal 0.M + Math.Round (decimal 0.06) |> equal 0.M + Math.Round (decimal 0.0836, 2) |> equal 0.08M + Math.Round (decimal -0.0836, 3) |> equal -0.084M + Math.Truncate (decimal -0.0836) |> equal 0.M + Math.Floor (0.M - 0.0836M) |> equal -1.M + +[] +let ``Decimal values below 0.1 hash the same however they are built`` () = + (decimal -0.0836).GetHashCode() |> equal ((0.M - 0.0836M).GetHashCode()) + (decimal "0.05").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + let set = Collections.Generic.HashSet() + set.Add(decimal -0.0836) |> equal true + set.Add(0.M - 0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[decimal "0.05"] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + +[] +let ``Decimal hash ignores trailing zeros and the sign of zero`` () = + (decimal "1.0").GetHashCode() |> equal ((decimal "1").GetHashCode()) + (decimal "0.050").GetHashCode() |> equal ((0.1M / 2.M).GetHashCode()) + (decimal "0.00").GetHashCode() |> equal ((decimal "0").GetHashCode()) + (-(0.M)).GetHashCode() |> equal ((0.M).GetHashCode()) + let set = Collections.Generic.HashSet([ 1.M ]) + set.Contains 1.0M |> equal true + set.Add 0.M |> equal true + set.Add(Math.Truncate -0.0836M) |> equal false + let dict = Collections.Generic.Dictionary() + dict.[0.050M] <- "a" + dict.ContainsKey(0.1M / 2.M) |> equal true + +[] +let ``Decimal parsing rounds to 28 places and keeps the scale of zero`` () = + string (decimal "0.0000000000000000000000000000004") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000005") |> equal "0.0000000000000000000000000000" + string (decimal "0.00000000000000000000000000015") |> equal "0.0000000000000000000000000002" + string (decimal "0.000000000000000000000000000012") |> equal "0.0000000000000000000000000000" + Decimal.GetBits(decimal "0.0000000000000000000000000000004") |> equal [| 0; 0; 0; 1835008 |] + Decimal.GetBits(decimal "0.00") |> equal [| 0; 0; 0; 131072 |] + string (-(0.00M)) |> equal "0.00" + string (abs 0.00M) |> equal "0.00" + string (Math.Round(0.004M, 2)) |> equal "0.00" + (0.00M).ToString("E3", CultureInfo.InvariantCulture) |> equal "0.000E+000" + (decimal "0.00").GetHashCode() |> equal (Decimal(0, 0, 0, false, 2uy).GetHashCode()) + +[] +let ``Decimal parsing handles precision limits and resets failed results`` () = + string (decimal "1.12345678901234567890123456789") |> equal "1.1234567890123456789012345679" + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + let mutable result = 1M + Decimal.TryParse("invalid", &result) |> equal false + result |> equal 0M + +[] +let ``Decimal parsing rounds long fractions with carry and at the mantissa limit`` () = + string (decimal ("0." + "9999999" + "9999999" + "9999999" + "9999999" + "5")) + |> equal ("1." + "0000000" + "0000000" + "0000000" + "0000000") + string (decimal ("7922816251426433759354395033.5" + "0000000" + "0000000" + "0000000" + "0000000")) + |> equal "7922816251426433759354395033.5" + Decimal.TryParse("79228162514264337593543950335.5" + "0000000" + "0000000" + "0000000" + "0000000") + |> fst + |> equal false + string (decimal ("000000000000000000000000000000001.25")) |> equal "1.25" + string (decimal ("0.00000000000000000000000000005" + String.replicate 1000 "0" + "1")) + |> equal "0.0000000000000000000000000001" + +[] +let ``Decimal addition and subtraction keep the larger scale`` () = + string (1.50M + 0.5M) |> equal "2.00" + string (1.5M + 0.50M) |> equal "2.00" + string (0.1M + 0.2M) |> equal "0.3" + string (1M + 1M) |> equal "2" + string (0.00M + 0M) |> equal "0.00" + string (0.00M + 1.5M) |> equal "1.50" + string (1.0M + (-1.0M)) |> equal "0.0" + string (0.05M - 0.05M) |> equal "0.00" + string (1.00M - 0.50M) |> equal "0.50" + string (100.5M - 0.5M) |> equal "100.0" + string (1.5M - 0.00M) |> equal "1.50" + +[] +let ``Decimal multiplication adds the scales up to 28 places`` () = + string (2.0M * 3.0M) |> equal "6.00" + string (1.5M * 2M) |> equal "3.0" + string (0.5M * 0.5M) |> equal "0.25" + string (1.50M * 1.0M) |> equal "1.500" + string (10M * 0.1M) |> equal "1.0" + string (1.10M * 1.10M) |> equal "1.2100" + string (0.0M * 5M) |> equal "0.0" + string (0M * 1.50M) |> equal "0.00" + string (pown 1.50M 2) |> equal "2.2500" + string (0.1234567890123456789012345678M * 0.1M) |> equal "0.0123456789012345678901234568" + string (0.0000000000000000000000000025M * 0.1M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000035M * 0.1M) |> equal "0.0000000000000000000000000004" + string (123456789012345678901234567.89M * 1.1M) |> equal "135802467913580246791358024.68" + +[] +let ``Decimal division keeps the scale of an exact quotient`` () = + string (1.00M / 2M) |> equal "0.50" + string (10M / 2M) |> equal "5" + string (1M / 4M) |> equal "0.25" + string (1.0M / 0.5M) |> equal "2" + string (2M / 1.0M) |> equal "2" + string (5.0M / 2.0M) |> equal "2.5" + string (1.000M / 8M) |> equal "0.125" + string (1.23M / 0.1M) |> equal "12.3" + string (0.0M / 5M) |> equal "0.0" + +[] +let ``Decimal division rounds an inexact quotient half to even to fit a decimal`` () = + string (1M / 3M) |> equal "0.3333333333333333333333333333" + string (2M / 3M) |> equal "0.6666666666666666666666666667" + string (10M / 3M) |> equal "3.3333333333333333333333333333" + string (100M / 3M) |> equal "33.333333333333333333333333333" + string (1M / 30000M) |> equal "0.0000333333333333333333333333" + string (1M / 7M) |> equal "0.1428571428571428571428571429" + string (80M / 9M) |> equal "8.888888888888888888888888889" + string (1000000000000000000000000000M / 3M) |> equal "333333333333333333333333333.33" + string (10000000000000000000000000001M / 2M) |> equal "5000000000000000000000000000.5" + string (0.0000000000000000000000000005M / 2M) |> equal "0.0000000000000000000000000002" + string (0.0000000000000000000000000007M / 2M) |> equal "0.0000000000000000000000000004" + string (0.0000000000000000000000000001M / 2M) |> equal "0" + +[] +let ``Decimal remainder keeps the larger scale`` () = + string (5.0M % 2M) |> equal "1.0" + string (5M % 2.0M) |> equal "1.0" + string (5.5M % 2M) |> equal "1.5" + string (7.00M % 2.5M) |> equal "2.00" + string (-5.0M % 2M) |> equal "-1.0" + string (1.5M % 2.00M) |> equal "1.5" + +[] +let ``Decimal arithmetic and parsing throw on overflow`` () = + (fun () -> Decimal.MaxValue + 1M) |> throwsAnyError + (fun () -> Decimal.MaxValue + 0.5M) |> throwsAnyError + string (Decimal.MaxValue + 0.4M) |> equal "79228162514264337593543950335" + string ((Decimal.MaxValue - 1M) + 1M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MinValue - 1M) |> throwsAnyError + (fun () -> Decimal.MaxValue * 1.1M) |> throwsAnyError + string (Decimal.MaxValue * 1.0M) |> equal "79228162514264337593543950335" + (fun () -> Decimal.MaxValue / 0.5M) |> throwsAnyError + (fun () -> decimal "1e30") |> throwsAnyError + (fun () -> decimal "79228162514264337593543950336") |> throwsAnyError + (fun () -> decimal "79228162514264337593543950335.5") |> throwsAnyError + string (decimal "79228162514264337593543950335.4") |> equal "79228162514264337593543950335" + (fun () -> decimal 1e30) |> throwsAnyError + +[] +let ``Decimal from float rounds to 15 significant digits`` () = + let inv = CultureInfo.InvariantCulture + string (decimal 0.1) |> equal "0.1" + string (decimal (0.1 + 0.2)) |> equal "0.3" + string (decimal 123456789.123456789) |> equal "123456789.123457" + string (decimal (1.0 / 3.0)) |> equal "0.333333333333333" + string (decimal 123456789012345.6789) |> equal "123456789012346" + string (decimal 123456789012345678.0) |> equal "123456789012346000" + string (decimal 1e20) |> equal "100000000000000000000" + string (decimal 1e28) |> equal "10000000000000000000000000000" + string (decimal 1e-10) |> equal "0.0000000001" + string (decimal 1.5e-28) |> equal "0.0000000000000000000000000002" + string (decimal 5e-29) |> equal "0" + string (decimal 1e-30) |> equal "0" + string (decimal -0.0) |> equal "0" + string (decimal 100000000000001.5) |> equal "100000000000002" + string (decimal 100000000000002.5) |> equal "100000000000002" + (fun () -> decimal 1e29) |> throwsAnyError + (fun () -> decimal nan) |> throwsAnyError + (fun () -> decimal infinity) |> throwsAnyError + +[] +let ``Decimal from float32 rounds to 7 significant digits`` () = + let inv = CultureInfo.InvariantCulture + string (decimal 1.1f) |> equal "1.1" + string (decimal (0.1f + 0.2f)) |> equal "0.3" + string (decimal 16777216.f) |> equal "16777220" + string (decimal 1.23456789f) |> equal "1.234568" + string (decimal 123456.789f) |> equal "123456.8" + string (decimal 1000001.5f) |> equal "1000002" + string (decimal 1000002.5f) |> equal "1000002" + string (decimal 1e-30f) |> equal "0" + (fun () -> decimal 3.4e38f) |> throwsAnyError + +[] +let ``Decimal.Parse follows NumberStyles`` () = + let inv = CultureInfo.InvariantCulture + Decimal.Parse("+5", inv) |> equal 5M + Decimal.Parse(" 5 ", inv) |> equal 5M + Decimal.Parse("5-", inv) |> equal -5M + Decimal.Parse("1,234.5", inv) |> equal 1234.5M + Decimal.Parse("1,23,4", inv) |> equal 1234M + Decimal.Parse(".5", inv) |> equal 0.5M + Decimal.Parse("5.", inv) |> equal 5M + string (Decimal.Parse("-0.00", inv)) |> equal "0.00" + (fun () -> Decimal.Parse("1e3", inv)) |> throwsAnyError + (fun () -> Decimal.Parse(",5", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1,234.5,6", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("(5)", inv)) |> throwsAnyError + (fun () -> Decimal.Parse("", inv)) |> throwsAnyError + Decimal.Parse("1.5E+2", NumberStyles.Float, inv) |> equal 150M + string (Decimal.Parse("1.50e1", NumberStyles.Float, inv)) |> equal "15.0" + Decimal.Parse("1e3", NumberStyles.Any, inv) |> equal 1000M + Decimal.Parse("(5)", NumberStyles.Any, inv) |> equal -5M + (fun () -> Decimal.Parse("1,234", NumberStyles.Float, inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1e30", NumberStyles.Float, inv)) |> throwsAnyError + let ok1, v1 = Decimal.TryParse("1,5", NumberStyles.Number, inv) + ok1 |> equal true + v1 |> equal 15M + let ok2, _ = Decimal.TryParse("1e3", NumberStyles.Number, inv) + ok2 |> equal false + +[] +let ``Decimal parsing handles grouping currency and extreme exponents`` () = + let inv = CultureInfo.InvariantCulture + Decimal.Parse("1,,2", inv) |> equal 12M + Decimal.Parse("1,", inv) |> equal 1M + Decimal.Parse("1,.2", inv) |> equal 1.2M + string (Decimal.Parse("1e-1001", NumberStyles.Float, inv)) |> equal "0.0000000000000000000000000000" + Decimal.Parse("0e1001", NumberStyles.Float, inv) |> equal 0M + let hugeExponent = String.replicate 1000 "9" + string (Decimal.Parse("1e-" + hugeExponent, NumberStyles.Float, inv)) + |> equal "0.0000000000000000000000000000" + Decimal.TryParse("1e" + hugeExponent, NumberStyles.Float, inv) |> fst |> equal false + Decimal.TryParse("0e" + hugeExponent + "x", NumberStyles.Float, inv) |> fst |> equal false + Decimal.TryParse("0e-" + hugeExponent + "x", NumberStyles.Float, inv) |> fst |> equal false + Decimal.Parse("1" + String.replicate 1001 "0" + "e-1001", NumberStyles.Float, inv) |> equal 1M + Decimal.Parse("0." + String.replicate 1000 "0" + "1e1001", NumberStyles.Float, inv) |> equal 1M + Decimal.Parse("\u00A41", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("-\u00A41", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4-1", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("1\u00A4-", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 1", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("1 \u00A4", NumberStyles.Any, inv) |> equal 1M + Decimal.Parse("(1 )", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 (1)", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("(1)\u00A4", NumberStyles.Any, inv) |> equal -1M + Decimal.Parse("\u00A4 ( 1 )", NumberStyles.Any, inv) |> equal -1M + Decimal.TryParse("( 1)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("( \u00A41)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("(1-)", NumberStyles.Any, inv) |> fst |> equal false + Decimal.TryParse("\u00A41\u00A4", NumberStyles.Any, inv) |> fst |> equal false + (fun () -> Decimal.Parse("$1", NumberStyles.Any, inv)) |> throwsAnyError + (fun () -> Decimal.Parse("1", NumberStyles.AllowHexSpecifier, inv)) |> throwsAnyError + (fun () -> Decimal.TryParse("1", enum 0x4000, inv) |> ignore) |> throwsAnyError + +[] +let ``Decimal parsing preserves zero sign and validates whitespace`` () = + let inv = CultureInfo.InvariantCulture + for text in [ "-0"; "-0.00"; "-0.00000000000000000000000000001"; "-0e-99"; "(0)"; "0-" ] do + let value = Decimal.Parse(text, NumberStyles.Any, inv) + Decimal.IsNegative(value) |> equal true + Decimal.GetBits(Decimal.Parse("-0.00", inv))[3] |> equal (Int32.MinValue ||| (2 <<< 16)) + Decimal.TryParse("\u00a01\u00a0", NumberStyles.Number, inv) |> fst |> equal false + Decimal.TryParse("\u20031\u2003", NumberStyles.Number, inv) |> fst |> equal false + Decimal.Parse("1 \000\000", NumberStyles.Number, inv) |> equal 1M + Decimal.TryParse("1\000 ", NumberStyles.Number, inv) |> fst |> equal false + +[] +let ``F# decimal on strings follows NumberStyles.Float`` () = + decimal "1e3" |> equal 1000M + string (decimal "1.50e1") |> equal "15.0" + string (decimal "0.0e1") |> equal "0" + decimal "+.5" |> equal 0.5M + decimal "5." |> equal 5M + decimal "5\000" |> equal 5M + string (decimal "1.50") |> equal "1.50" + (fun () -> decimal "1,000") |> throwsAnyError + (fun () -> decimal "5-") |> throwsAnyError + (fun () -> decimal "(5)") |> throwsAnyError + (fun () -> decimal "1e") |> throwsAnyError + (fun () -> decimal (nan: float)) |> throwsAnyError + (fun () -> decimal (infinity: float)) |> throwsAnyError + +[] +let ``Decimal G format keeps the scale without a precision`` () = + let inv = CultureInfo.InvariantCulture + (1.10M).ToString("G", inv) |> equal "1.10" + (0.00M).ToString("G", inv) |> equal "0.00" + (1.10M).ToString("G5", inv) |> equal "1.1" + (123456M).ToString("G3", inv) |> equal "1.23E+05" + (0.0001234M).ToString("G3", inv) |> equal "0.000123" + (2.5M).ToString("G1", inv) |> equal "3" + (1.5M).ToString("G1", inv) |> equal "2" + +[] +let ``Decimal max returns the first of two equal values`` () = + let inv = CultureInfo.InvariantCulture + string (max 1.0M 1.00M) |> equal "1.0" + string (min 1.0M 1.00M) |> equal "1.00" + string (Math.Max(1.0M, 1.00M)) |> equal "1.0" + string (Math.Min(1.0M, 1.00M)) |> equal "1.00" + +[] +let ``Decimal static conversions truncate and check the range`` () = + Decimal.ToInt32 2.9M |> equal 2 + Decimal.ToInt32 -2.9M |> equal -2 + Decimal.ToInt64 -2.9M |> equal -2L + Decimal.ToInt64 9223372036854775807.5M |> equal 9223372036854775807L + Decimal.ToUInt64 18446744073709551615M |> equal 18446744073709551615UL + Decimal.ToInt16 32767.5M |> equal 32767s + Decimal.ToUInt16 65535.9M |> equal 65535us + Decimal.ToByte 255.9M |> equal 255uy + Decimal.ToSByte -128.9M |> equal -128y + Decimal.ToUInt32 4294967295.9M |> equal 4294967295u + Decimal.ToDouble 0.1M |> equal 0.1 + Decimal.ToSingle 1.5M |> equal 1.5f + (fun () -> Decimal.ToInt32 2147483648M) |> throwsAnyError + (fun () -> Decimal.ToByte -1M) |> throwsAnyError + Decimal.ToUInt64 -0.5M |> equal 0UL + +[] +let ``Decimal rounding keeps the requested scale`` () = + string (Math.Round(1.20001M, 2)) |> equal "1.20" + string (Math.Round(1.2M, 3)) |> equal "1.2" + string (Math.Round(1.99M, 1)) |> equal "2.0" + string (Math.Floor 1.50M) |> equal "1" + string (Math.Ceiling 1.50M) |> equal "2" + string (Math.Truncate 1.50M) |> equal "1" + +[] +let ``Decimal rounding is half to even when a trailing zero follows the 5`` () = + Math.Round 2.50M |> equal 2.M + Math.Round 0.50M |> equal 0.M + Math.Round(1.250M, 1) |> equal 1.2M + Math.Round(0.050M, 1) |> equal 0.0M + Math.Round(0.0050M, 2) |> equal 0.00M + [] let ``Decimal pown works`` () = pown 2.2M 3 |> equal 10.648M @@ -700,14 +1079,14 @@ let ``Double comparison works`` () = // [] // let ``Double.Parse works with IFormatProvider`` () = // // culture compiles to { } for now and it is ignored on the call-site -// let culture = Globalization.CultureInfo.InvariantCulture +// let culture = CultureInfo.InvariantCulture // let result = Double.Parse("10.5", culture) // result |> equal 10.5 // [] // let ``Single.Parse works with IFormatProvider`` () = // // culture compiles to { } for now and it is ignored on the call-site -// let culture = Globalization.CultureInfo.InvariantCulture +// let culture = CultureInfo.InvariantCulture // let result = Single.Parse("10.5", culture) // float result |> equal 10.5 diff --git a/tests/Rust/tests/src/ConvertTests.fs b/tests/Rust/tests/src/ConvertTests.fs index 2626b50b5d..2c7fd42ae6 100644 --- a/tests/Rust/tests/src/ConvertTests.fs +++ b/tests/Rust/tests/src/ConvertTests.fs @@ -45,6 +45,28 @@ let ``System.Decimal.TryParse works`` () = Decimal.TryParse "9SayWhat12Huh" |> equal (false, 0.0M) Decimal.TryParse "-1.5" |> equal (true, -1.5M) +[] +let ``System.Decimal.TryParse handles null strings`` () = + let text: string = null + let mutable result = 42M + Decimal.TryParse(text, &result) |> equal false + result |> equal 0M + result <- 42M + Decimal.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, &result) |> equal false + result |> equal 0M + result <- 42M + (fun () -> Decimal.TryParse(text, enum 0x4000, CultureInfo.InvariantCulture, &result) |> ignore) + |> throwsAnyError + result |> equal 42M + +[] +let ``System.Decimal.Parse handles null strings`` () = + let text: string = null + (fun () -> Decimal.Parse(text)) |> throwsAnyError + (fun () -> Decimal.Parse(text, CultureInfo.InvariantCulture)) |> throwsAnyError + (fun () -> decimal text) |> throwsAnyError + decimal "1.50e1" |> equal 15M + [] let ``System.Single.Parse works`` () = Single.Parse "1.5" |> equal 1.5f @@ -767,6 +789,24 @@ let ``System.Convert.ToDecimal works`` () = Convert.ToDecimal(1.m) |> equal x Convert.ToDecimal("1.") |> equal x +[] +let ``System.Convert.ToDecimal string uses NumberStyles.Number`` () = + Convert.ToDecimal("1,000") |> equal 1000M + Convert.ToDecimal("1-") |> equal (-1M) + Convert.ToDecimal("1000,") |> equal 1000M + Convert.ToDecimal("1,,000") |> equal 1000M + Convert.ToDecimal("1,000", CultureInfo.InvariantCulture) |> equal 1000M + (fun () -> Convert.ToDecimal("1e3")) |> throwsAnyError + (fun () -> Convert.ToDecimal(",1")) |> throwsAnyError + +[] +let ``System.Convert.ToDecimal handles null strings`` () = + let text: string = null + Convert.ToDecimal(text) |> equal 0M + Convert.ToDecimal(text, CultureInfo.InvariantCulture) |> equal 0M + Convert.ToDecimal("1,234.50", CultureInfo.InvariantCulture) |> equal 1234.50M + (fun () -> Convert.ToDecimal("invalid")) |> throwsAnyError + //------------------------------------- // Convert.ToString //------------------------------------- diff --git a/tests/Rust/tests/src/RuneTests.fs b/tests/Rust/tests/src/RuneTests.fs index cfb2b2223a..5bdcfb0a33 100644 --- a/tests/Rust/tests/src/RuneTests.fs +++ b/tests/Rust/tests/src/RuneTests.fs @@ -273,8 +273,8 @@ let ``Rune.Equals works`` () = [] let ``Rune.GetHashCode works`` () = - Rune('a').GetHashCode() |> equal 97 - Rune(0x1F600).GetHashCode() |> equal 0x1F600 + Rune('a').GetHashCode() |> equal (Rune(97).GetHashCode()) + Rune(0x1F600).GetHashCode() |> equal (Rune.GetRuneAt("😀", 0).GetHashCode()) [] let ``Rune.GetRuneAt works with BMP characters`` () = From 5a7ea990b76c928564364f4844fc9a3666b612fc Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sun, 1 Nov 2026 12:01:37 +0100 Subject: [PATCH 289/322] fix(js/ts): support MidpointRounding in Math.Round (#5233) --- src/Fable.Transforms/Replacements.fs | 24 +++++--- src/fable-library-ts/Decimal.ts | 20 ++++++- src/fable-library-ts/Util.ts | 57 ++++++++++++++++++ tests/Js/Main/ArithmeticTests.fs | 88 ++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 9c640fc58c..81b7c0b7d5 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -465,6 +465,19 @@ let round com (args: Expr list) = rounded :: args.Tail | _ -> args +let roundCall (com: ICompiler) r t moduleName (i: CallInfo) (args: Expr list) = + let isMidpointRounding = + match List.tryLast i.SignatureArgTypes with + | Some(Number(_, NumberInfo.IsEnum ent)) -> ent.FullName = "System.MidpointRounding" + | _ -> false + + match args with + | [ _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundMode", t, args, ?loc = r) + | [ ExprType(Number(Float32, _)); _; _ ] when isMidpointRounding -> + Helper.LibCall(com, moduleName, "roundToModeFloat32", t, args, ?loc = r) + | [ _; _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundToMode", t, args, ?loc = r) + | _ -> Helper.LibCall(com, moduleName, "round", t, args, i.SignatureArgTypes, ?loc = r) + let toList com returnType expr = Helper.LibCall(com, "List", "ofSeq", returnType, [ expr ]) @@ -1657,12 +1670,8 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | _ -> applyOp com ctx r t i.CompiledName args |> Some | "Round", _ -> match args with - | ExprType(Number(Decimal, _)) :: _ -> - Helper.LibCall(com, "Decimal", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some - | _ -> - Helper.LibCall(com, "Util", "round", t, args, i.SignatureArgTypes, ?thisArg = thisArg, ?loc = r) - |> Some + | ExprType(Number(Decimal, _)) :: _ -> roundCall com r t "Decimal" i args |> Some + | _ -> roundCall com r t "Util" i args |> Some | "Truncate", _ -> match args with | ExprType(Number(Decimal, _)) :: _ -> @@ -2950,7 +2959,8 @@ let decimals (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: | Decimal -> toDecimal com ctx r t args |> Some | _ -> None | _ -> None - | ("Ceiling" | "Floor" | "Round" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), + | "Round", _ -> roundCall com r t "Decimal" i args |> Some + | ("Ceiling" | "Floor" | "Truncate" | "Min" | "Max" | "MinMagnitude" | "MaxMagnitude" | "Clamp" | "Add" | "Subtract" | "Multiply" | "Divide" | "Remainder" | "Negate" as meth), _ -> let meth = Naming.lowerFirst meth diff --git a/src/fable-library-ts/Decimal.ts b/src/fable-library-ts/Decimal.ts index c9f2a42019..f12cda7161 100644 --- a/src/fable-library-ts/Decimal.ts +++ b/src/fable-library-ts/Decimal.ts @@ -1,7 +1,7 @@ import Decimal, { BigSource } from "./lib/big.js"; import { Numeric, symbol } from "./Numeric.ts"; import { FSharpRef } from "./Types.ts"; -import { Exception, combineHashCodes } from "./Util.ts"; +import { Exception, MidpointRounding, combineHashCodes } from "./Util.ts"; import { NumberStyles, splitNumber } from "./NumberStyles.ts"; import { ArgumentNullException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5, OverflowException_$ctor_Z721C83C5 } from "./System.ts"; import { int8, uint8, int16, uint16, int32, uint32, float16, float32, float64 } from "./Int32.ts"; @@ -73,6 +73,24 @@ export function round(x: Decimal, digits: number = 0) { return x.round(digits, 2 /* ROUND_HALF_EVEN */); } +export function roundToMode(x: Decimal, digits: number, mode: number) { + if (!(digits >= 0 && digits <= 28)) { + throw new Exception("Decimal can only round to between 0 and 28 digits of precision. (Parameter 'decimals')"); + } + switch (mode) { + case MidpointRounding.ToEven: return x.round(digits, 2 /* ROUND_HALF_EVEN */); + case MidpointRounding.AwayFromZero: return x.round(digits, 1 /* ROUND_HALF_UP */); + case MidpointRounding.ToZero: return x.round(digits, 0 /* ROUND_DOWN */); + case MidpointRounding.ToNegativeInfinity: return x.round(digits, x.cmp(0) >= 0 ? 0 /* ROUND_DOWN */ : 3 /* ROUND_UP */); + case MidpointRounding.ToPositiveInfinity: return x.round(digits, x.cmp(0) >= 0 ? 3 /* ROUND_UP */ : 0 /* ROUND_DOWN */); + default: throw new Exception(`The value '${mode}' is not valid for this usage of the type MidpointRounding. (Parameter 'mode')`); + } +} + +export function roundMode(x: Decimal, mode: number) { + return roundToMode(x, 0, mode); +} + export function truncate(x: Decimal) { return x.round(0, 0 /* ROUND_DOWN */); } diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index d8c3322424..343b8f2283 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -714,6 +714,63 @@ export function round(value: number, digits: number = 0) { return digits ? r / m : r; } +export const MidpointRounding = { + ToEven: 0, + AwayFromZero: 1, + ToZero: 2, + ToNegativeInfinity: 3, + ToPositiveInfinity: 4, +} as const; + +function invalidMidpointRounding(mode: number) { + return new Exception(`The value '${mode}' is not valid for this usage of the type MidpointRounding. (Parameter 'mode')`); +} + +function roundToModeWithLimits(value: number, digits: number, mode: number, maxDigits: number, limit: number): number { + if (!(digits >= 0 && digits <= maxDigits)) { + throw new Exception(`Rounding digits must be between 0 and ${maxDigits}, inclusive. (Parameter 'digits')`); + } + // .NET returns values at or above the limit unchanged, without validating mode + if (!(Math.abs(value) < limit)) { + return value; + } + const m = Math.pow(10, digits); + let v = value * m; + switch (mode) { + case MidpointRounding.ToEven: { + const r = Math.round(v); + v = r - v === 0.5 && r % 2 !== 0 ? r - 1 : r; + break; + } + case MidpointRounding.AwayFromZero: { + const i = Math.trunc(v); + const f = v - i; + v = Math.abs(f) >= 0.5 ? i + Math.sign(f) : i; + break; + } + case MidpointRounding.ToZero: v = Math.trunc(v); break; + case MidpointRounding.ToNegativeInfinity: v = Math.floor(v); break; + case MidpointRounding.ToPositiveInfinity: v = Math.ceil(v); break; + default: throw invalidMidpointRounding(mode); + } + return v / m; +} + +export function roundMode(value: number, mode: number): number { + if (!(mode >= MidpointRounding.ToEven && mode <= MidpointRounding.ToPositiveInfinity)) { + throw invalidMidpointRounding(mode); + } + return roundToModeWithLimits(value, 0, mode, 15, 1e16); +} + +export function roundToMode(value: number, digits: number, mode: number): number { + return roundToModeWithLimits(value, digits, mode, 15, 1e16); +} + +export function roundToModeFloat32(value: number, digits: number, mode: number): number { + return roundToModeWithLimits(value, digits, mode, 6, 1e8); +} + export function sign(x: number): number { return x > 0 ? 1 : x < 0 ? -1 : 0; } diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index cd08db9a78..4848dcbf49 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -333,6 +333,29 @@ let tests = Math.Round(-1.426M, 2) |> equal -1.43M Math.Round(-1.426M, 1) |> equal -1.4M + testCase "Decimal round with MidpointRounding works" <| fun () -> + let round mode = [ 2.5M; -2.5M; 3.5M; -3.5M; 2.4M; -2.6M ] |> List.map (fun x -> Math.Round(x, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 2.M; -2.M; 4.M; -4.M; 2.M; -3.M ] + round MidpointRounding.AwayFromZero |> equal [ 3.M; -3.M; 4.M; -4.M; 2.M; -3.M ] + round MidpointRounding.ToZero |> equal [ 2.M; -2.M; 3.M; -3.M; 2.M; -2.M ] + round MidpointRounding.ToNegativeInfinity |> equal [ 2.M; -3.M; 3.M; -4.M; 2.M; -3.M ] + round MidpointRounding.ToPositiveInfinity |> equal [ 3.M; -2.M; 4.M; -3.M; 3.M; -2.M ] + let roundDigits mode = [ 1.25M, 1; -1.25M, 1; 1.235M, 2; -1.235M, 2; 1.426M, 2 ] |> List.map (fun (x, d) -> Math.Round(x, d, (mode: MidpointRounding))) + roundDigits MidpointRounding.ToEven |> equal [ 1.2M; -1.2M; 1.24M; -1.24M; 1.43M ] + roundDigits MidpointRounding.AwayFromZero |> equal [ 1.3M; -1.3M; 1.24M; -1.24M; 1.43M ] + roundDigits MidpointRounding.ToZero |> equal [ 1.2M; -1.2M; 1.23M; -1.23M; 1.42M ] + roundDigits MidpointRounding.ToNegativeInfinity |> equal [ 1.2M; -1.3M; 1.23M; -1.24M; 1.42M ] + roundDigits MidpointRounding.ToPositiveInfinity |> equal [ 1.3M; -1.2M; 1.24M; -1.23M; 1.43M ] + Decimal.Round(2.5M, MidpointRounding.AwayFromZero) |> equal 3.M + Decimal.Round(-1.25M, 1, MidpointRounding.AwayFromZero) |> equal -1.3M + + testCase "Decimal round with MidpointRounding validates its arguments" <| fun () -> + (fun () -> Math.Round(1.5M, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1.5M, 2, enum -1)) |> throwsAnyError + (fun () -> Math.Round(1.5M, 29, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> Math.Round(1.5M, -1, MidpointRounding.AwayFromZero)) |> throwsAnyError + Math.Round(1.5M, 28, MidpointRounding.AwayFromZero) |> equal 1.5M + testCase "Decimal truncate works" <| fun () -> truncate 11.0M |> equal 11.M truncate 11.01M |> equal 11.M @@ -880,6 +903,71 @@ let tests = Math.Round(-1.426, 2) |> equal -1.43 Math.Round(-1.426, 1) |> equal -1.4 + testCase "Math.Round with MidpointRounding works" <| fun () -> + let round mode = [ 2.5; -2.5; 3.5; -3.5; 2.4; -2.6; 0.5; -0.5 ] |> List.map (fun x -> Math.Round(x, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 2.; -2.; 4.; -4.; 2.; -3.; 0.; -0. ] + round MidpointRounding.AwayFromZero |> equal [ 3.; -3.; 4.; -4.; 2.; -3.; 1.; -1. ] + round MidpointRounding.ToZero |> equal [ 2.; -2.; 3.; -3.; 2.; -2.; 0.; -0. ] + round MidpointRounding.ToNegativeInfinity |> equal [ 2.; -3.; 3.; -4.; 2.; -3.; 0.; -1. ] + round MidpointRounding.ToPositiveInfinity |> equal [ 3.; -2.; 4.; -3.; 3.; -2.; 1.; -0. ] + Math.Round(0.49999999999999994, MidpointRounding.AwayFromZero) |> equal 0. + Math.Round(4503599627370497., MidpointRounding.AwayFromZero) |> equal 4503599627370497. + 1. / Math.Round(-0.4, MidpointRounding.AwayFromZero) |> equal -infinity + + testCase "Math.Round with digits and MidpointRounding works" <| fun () -> + let round mode = [ 1.25, 1; -1.25, 1; 1.235, 2; 2.675, 2; -2.675, 2; 1.426, 2; 0.125, 2; -1.005, 2 ] |> List.map (fun (x, d) -> Math.Round(x, d, (mode: MidpointRounding))) + round MidpointRounding.ToEven |> equal [ 1.2; -1.2; 1.24; 2.68; -2.68; 1.43; 0.12; -1. ] + round MidpointRounding.AwayFromZero |> equal [ 1.3; -1.3; 1.24; 2.68; -2.68; 1.43; 0.13; -1. ] + round MidpointRounding.ToZero |> equal [ 1.2; -1.2; 1.23; 2.67; -2.67; 1.42; 0.12; -1. ] + round MidpointRounding.ToNegativeInfinity |> equal [ 1.2; -1.3; 1.23; 2.67; -2.68; 1.42; 0.12; -1.01 ] + round MidpointRounding.ToPositiveInfinity |> equal [ 1.3; -1.2; 1.24; 2.68; -2.67; 1.43; 0.13; -1. ] + Math.Round(1e17, 2, MidpointRounding.AwayFromZero) |> equal 1e17 + + testCase "Math.Round with MidpointRounding.ToEven rounds the exact value" <| fun () -> + Math.Round(2.500000001, MidpointRounding.ToEven) |> equal 3. + Math.Round(-2.500000001, MidpointRounding.ToEven) |> equal -3. + Math.Round(2.499999999, MidpointRounding.ToEven) |> equal 2. + Math.Round(1.015, 2, MidpointRounding.ToEven) |> equal 1.01 + Math.Round(1.015, 2, MidpointRounding.AwayFromZero) |> equal 1.01 + Math.Round(1.025, 2, MidpointRounding.ToEven) |> equal 1.02 + 1. / Math.Round(-0.5, MidpointRounding.ToEven) |> equal -infinity + 1. / Math.Round(-0.04, 1, MidpointRounding.ToEven) |> equal -infinity + + testCase "Math.Round with MidpointRounding keeps NaN and infinities" <| fun () -> + let modes = [ MidpointRounding.ToEven; MidpointRounding.AwayFromZero; MidpointRounding.ToZero; MidpointRounding.ToNegativeInfinity; MidpointRounding.ToPositiveInfinity ] + for mode in modes do + Double.IsNaN(Math.Round(nan, mode)) |> equal true + Double.IsNaN(Math.Round(nan, 2, mode)) |> equal true + Math.Round(infinity, mode) |> equal infinity + Math.Round(-infinity, 2, mode) |> equal -infinity + + testCase "Math.Round with MidpointRounding validates its arguments" <| fun () -> + (fun () -> Math.Round(1.5, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1e17, enum 5)) |> throwsAnyError + (fun () -> Math.Round(nan, enum 5)) |> throwsAnyError + (fun () -> Math.Round(1.5, 2, enum -1)) |> throwsAnyError + (fun () -> Math.Round(1.5, 16, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> Math.Round(1.5, -1, MidpointRounding.ToEven)) |> throwsAnyError + (fun () -> Math.Round(nan, 16, MidpointRounding.ToEven)) |> throwsAnyError + Math.Round(1.5, 15, MidpointRounding.AwayFromZero) |> equal 1.5 + + testCase "Math.Round with digits and MidpointRounding skips mode validation for unrounded values" <| fun () -> + Math.Round(1e17, 2, enum 5) |> equal 1e17 + Math.Round(infinity, 2, enum 5) |> equal infinity + Double.IsNaN(Math.Round(nan, 2, enum 5)) |> equal true + + testCase "MathF.Round with MidpointRounding works" <| fun () -> + MathF.Round(2.5f, MidpointRounding.AwayFromZero) |> equal 3.f + MathF.Round(-2.5f, MidpointRounding.AwayFromZero) |> equal -3.f + MathF.Round(2.5f, MidpointRounding.ToEven) |> equal 2.f + MathF.Round(2.5f, MidpointRounding.ToZero) |> equal 2.f + MathF.Round(-2.5f, MidpointRounding.ToNegativeInfinity) |> equal -3.f + MathF.Round(-2.5f, MidpointRounding.ToPositiveInfinity) |> equal -2.f + MathF.Round(1.5f, 6, MidpointRounding.AwayFromZero) |> equal 1.5f + MathF.Round(1e9f, 2, enum 5) |> equal 1e9f + (fun () -> MathF.Round(1.5f, 7, MidpointRounding.AwayFromZero)) |> throwsAnyError + (fun () -> MathF.Round(1e9f, enum 5)) |> throwsAnyError + testCase "Math.Truncate works" <| fun () -> Math.Truncate -12.5 |> equal -12. Math.Truncate 1.425 |> equal 1. From 86c000041f286668353615003692b07e9082ff01 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sun, 1 Nov 2026 17:47:31 +0100 Subject: [PATCH 290/322] fix(ts): declare Mangle(false) interface members unmangled (#5234) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 11 ++---- src/Fable.Transforms/FSharp2Fable.Util.fs | 5 +++ tests/TypeScript/LiteTsInteropTests.fs | 47 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index ea587c2ea5..bfd354ce76 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -4633,13 +4633,8 @@ but thanks to the optimisation done below we get let name = fullNamePath + genericSuffix + "." + memb.CompiledName + overloadSuffix name - let transformAbstractMember com ctx (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) = - // TODO: replicate the mangling logic from FSharp2Fable.Util.getAbstractMemberInfo - // let info = FSharp2Fable.Util.getAbstractMemberInfo com ent memb - let isMangled = - (ent.IsAbstractClass && not (hasAttribute Atts.attachMembers ent.Attributes)) - || hasAttribute Atts.mangle ent.Attributes - + let transformAbstractMember (com: IBabelCompiler) ctx (ent: Fable.Entity) (memb: Fable.MemberFunctionOrValue) = + let isMangled = FSharp2Fable.Util.isMangledAbstractFableEntity com ent let isGetter = memb.IsGetter let isSetter = memb.IsSetter @@ -4647,7 +4642,7 @@ but thanks to the optimisation done below we get if isMangled then getMangledMemberName ent memb elif isGetter || isSetter then - memb.CompiledName |> Naming.removeGetSetPrefix + memb.DisplayName else memb.CompiledName diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 736d28489d..9fcd034fa6 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -2737,6 +2737,11 @@ module Util = || isPojoDefinedByConsArgsFSharpEntity ent ) + let isMangledAbstractFableEntity (com: Compiler) (ent: Fable.Entity) = + match ent with + | :? FsEnt as ent -> isMangledAbstractEntity com ent.FSharpEntity + | _ -> false + let getMangledAbstractMemberName (ent: FSharpEntity) memberName overloadHash = // TODO: Error if entity doesn't have fullName? let entityName = defaultArg ent.TryFullName "" diff --git a/tests/TypeScript/LiteTsInteropTests.fs b/tests/TypeScript/LiteTsInteropTests.fs index 49d2adbeca..a33996f40f 100644 --- a/tests/TypeScript/LiteTsInteropTests.fs +++ b/tests/TypeScript/LiteTsInteropTests.fs @@ -55,6 +55,33 @@ module TypedArrays = let float64s = JS.Constructors.Float64Array.Create [| 1.5; -2.5 |] let bigInt64s = JS.Constructors.BigInt64Array.Create 2 let makeFloat32s () = JS.Constructors.Float32Array.Create [| 0.0; 1.0 |] + +// tsc checks the declarations against the unmangled names the JavaScript uses, see #5158 +module MangleInterfaces = + [] + type INotMangled = + abstract Foo: int -> int + abstract Bar: string + abstract Baz: int with get, set + + [] + type IMangled = + abstract Foo: int -> int + + let useNotMangled (x: INotMangled) = x.Foo 1, x.Bar + let incrementBaz (x: INotMangled) = x.Baz <- x.Baz + 1 + let useMangled (x: IMangled) = x.Foo 1 + +module MangleAbstractClasses = + [] + type AttachedBase() = + abstract Run: int -> int + + type AttachedImpl() = + inherit AttachedBase() + override _.Run x = x + 3 + + let useAttached (x: AttachedBase) = x.Run 1 #endif type GenericNameRecord<'a> = @@ -128,5 +155,25 @@ let tests = TypedArrays.float64s.[1] |> equal -2.5 TypedArrays.bigInt64s.length |> equal 2 TypedArrays.makeFloat32s().[1] |> equal 1.0f + + testCase "Interfaces with [] are declared with unmangled members" <| fun () -> + let baz = ref 1 + let notMangled = + { new MangleInterfaces.INotMangled with + member _.Foo x = x + 1 + member _.Bar = "bar" + member _.Baz with get () = baz.Value and set v = baz.Value <- v } + MangleInterfaces.useNotMangled notMangled |> equal (2, "bar") + notMangled?Bar |> equal "bar" + MangleInterfaces.incrementBaz notMangled + notMangled.Baz |> equal 2 + notMangled?Baz |> equal 2 + let mangled = { new MangleInterfaces.IMangled with member _.Foo x = x + 2 } + MangleInterfaces.useMangled mangled |> equal 3 + + testCase "Abstract classes with [] ignore [] in declarations" <| fun () -> + let attached = MangleAbstractClasses.AttachedImpl() + MangleAbstractClasses.useAttached attached |> equal 4 + attached?Run(2) |> equal 5 #endif ] From 6c22c162674a167e43e49ad0698f426123bc35f4 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 1 Nov 2026 17:59:21 +0100 Subject: [PATCH 291/322] fix(js/ts): print null strings as empty --- src/Fable.Transforms/Replacements.fs | 24 ++++++++++++--- src/fable-library-ts/String.ts | 4 ++- src/fable-library-ts/Types.ts | 4 +++ .../optionalArgs/OptionalArgs.jsx.expected | 4 +-- tests/Js/Main/StringTests.fs | 30 +++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 81b7c0b7d5..f49ec20666 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -421,14 +421,18 @@ let toChar com (ctx: Context) r (arg: Expr) = let code = toInt com ctx r UInt16.Number [ arg ] Helper.GlobalCall("String", Char, [ code ], memb = "fromCharCode") -let toString com (ctx: Context) r (args: Expr list) = +let private toStringWith nullAsEmpty com (ctx: Context) r (args: Expr list) = match args with | [] -> "toString is called with empty args" |> addErrorAndReturnNull com ctx.InlinePath r | head :: tail -> match head.Type with - | String -> head + | String -> + match head with + | Value(StringConstant _, _) -> head + | _ when nullAsEmpty -> Operation(Logical(LogicalOr, head, makeStrConst ""), Tags.empty, String, r) + | _ -> head | Char -> TypeCast(head, String) | Builtin BclGuid when tail.IsEmpty -> head | Builtin BclDateOnly -> Helper.LibCall(com, dateOnlyModule com, "toString", String, args) @@ -453,7 +457,19 @@ let toString com (ctx: Context) r (args: Expr list) = // | DeclaredType(ent, _) when ent.IsFSharpUnion || ent.IsFSharpRecord || ent.IsValueType -> // Helper.InstanceCall(head, "toString", String, [], ?loc=r) // | DeclaredType(ent, _) -> - | _ -> Helper.LibCall(com, "Types", "toString", String, [ head ], ?loc = r) + | _ -> + let funcName = + if nullAsEmpty then + "toStringOrEmpty" + else + "toString" + + Helper.LibCall(com, "Types", funcName, String, [ head ], ?loc = r) + +let toString com ctx r args = toStringWith false com ctx r args + +/// F#'s `string` operator returns an empty string for null +let stringOperator com ctx r args = toStringWith true com ctx r args let round com (args: Expr list) = match args.Head.Type with @@ -1566,7 +1582,7 @@ let operators (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr o | ("ToSingle" | "ToDouble"), _ -> toFloat com ctx r t args |> Some | "ToDecimal", _ -> toDecimal com ctx r t args |> Some | "ToChar", _ -> toChar com ctx r args.Head |> Some - | "ToString", _ -> toString com ctx r args |> Some + | "ToString", _ -> stringOperator com ctx r args |> Some | "CreateSequence", [ xs ] -> TypeCast(xs, t) |> Some | ("CreateDictionary" | "CreateReadOnlyDictionary"), [ arg ] -> makeDictionary com ctx r t arg |> Some | "CreateSet", _ -> (genArg com ctx r 0 i.GenericArgs) |> makeSet com ctx r t "OfSeq" args |> Some diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 79080b39a0..4213448d65 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -267,6 +267,8 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, rep = formatDateLike(rep, undefined); } else if (format === "A" && typeof rep === "string") { rep = "\"" + rep + "\""; + } else if (format === "s" && rep == null) { + rep = ""; } else { rep = toString(rep); } @@ -609,7 +611,7 @@ export function format(str: string | object, ...args: any[]) { } else if (rep instanceof Date || isNetDateFormattable(rep)) { rep = formatDateLike(rep, pattern || format); } else { - rep = toString(rep) + rep = rep == null ? "" : toString(rep); } padLength = parseInt((padLength || " ").substring(1), 10); if (!isNaN(padLength)) { diff --git a/src/fable-library-ts/Types.ts b/src/fable-library-ts/Types.ts index faf4c05718..629b2fe8ef 100644 --- a/src/fable-library-ts/Types.ts +++ b/src/fable-library-ts/Types.ts @@ -45,6 +45,10 @@ export function toString(x: any, callStack = 0): string { return String(x); } +export function toStringOrEmpty(x: any): string { + return x == null ? "" : toString(x); +} + export function unionToString(name: string, fields: any[]) { if (fields.length === 0) { return name; diff --git a/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected b/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected index 793aff95ad..e12cfc938a 100644 --- a/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected +++ b/tests/Integration/Integration/data/optionalArgs/OptionalArgs.jsx.expected @@ -1,6 +1,6 @@ import { class_type } from "./fable_modules/fable-library-js.5.4.0/Reflection.js"; import { int32ToString } from "./fable_modules/fable-library-js.5.4.0/Util.js"; -import { toString } from "./fable_modules/fable-library-js.5.4.0/Types.js"; +import { toStringOrEmpty } from "./fable_modules/fable-library-js.5.4.0/Types.js"; import { some, defaultArg } from "./fable_modules/fable-library-js.5.4.0/Option.js"; export function jsBinding(v, sp) { @@ -33,7 +33,7 @@ export function Native_$ctor() { } export function Native_Show_21A309C4(x, y) { - return int32ToString(x) + toString(defaultArg(y, "-")); + return int32ToString(x) + toStringOrEmpty(defaultArg(y, "-")); } export function fSharpMember(n) { diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index ce1e039aa0..f781bae0e5 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -122,6 +122,12 @@ let tests = testList "Strings" [ sb.Append(8.0f) |> ignore // float32 sb.ToString().Replace(",", ".") |> equal "12345678" + testCase "StringBuilder.Append treats null as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + let sb = System.Text.StringBuilder() + sb.Append("a").Append(n).Append(o).Append("b").ToString() |> equal "ab" + testCase "StringBuilder.AppendFormat works" <| fun () -> let sb = System.Text.StringBuilder() sb.AppendFormat("Hello{0}World{1}", " ", "!") |> ignore @@ -256,6 +262,14 @@ let tests = testList "Strings" [ testCase "sprintf without arguments works" <| fun () -> sprintf "hello" |> equal "hello" + testCase "sprintf \"%s\" prints null as empty" <| fun () -> // See #5162 + let n: string = null + sprintf "%s" n |> equal "" + sprintf "a%sb" n |> equal "ab" + sprintf "[%5s]" n |> equal "[ ]" + sprintf "[%-5s]" n |> equal "[ ]" + (try failwithf "x%sy" n with e -> e.Message) |> equal "xy" + testCase "input of print format can be retrieved" <| fun () -> let pathScan (pf:PrintfFormat<_,_,_,_,'t>) = let formatStr = pf.Value @@ -496,6 +510,13 @@ let tests = testList "Strings" [ testCase "String.Format should fail if there are less arguments than placeholders" <| fun () -> // See #2768 throwsAnyError <| fun () -> String.Format ("Hello {0}", args = Array.empty) + testCase "String.Format treats null arguments as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + String.Format("a{0}b", n) |> equal "ab" + String.Format("a{0}b", o) |> equal "ab" + String.Format("[{0,3}]", n) |> equal "[ ]" + testCase "String.Format combining padding and zeroes pattern works" <| fun () -> String.Format(CultureInfo.InvariantCulture, "{0:++0.00++}", -5000.5657) |> equal "-++5000.57++" String.Format(CultureInfo.InvariantCulture, "{0:000.00}foo", 5) |> equal "005.00foo" @@ -1299,6 +1320,15 @@ let tests = testList "Strings" [ String.Concat([| "a"; null; "b" |]) |> equal "ab" String.Concat(box "a", null, box 1) |> equal "a1" + testCase "string treats null as empty" <| fun () -> // See #5162 + let n: string = null + let o: obj = null + string n |> equal "" + string (box n) |> equal "" + string o |> equal "" + string (None: int option) |> equal "" + string "a" |> equal "a" + testCase "System.String.Concat works" <| fun () -> String.Concat("a", "b", "c") |> equal "abc" From d82ef8932c58ddc73ff5aa6ca669be86aedbe9db Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sun, 1 Nov 2026 23:06:41 +0100 Subject: [PATCH 292/322] fix(ts): extend library classes for BCL collection bases (#5240) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 5 +++++ tests/Js/Main/DictionaryTests.fs | 18 ++++++++++++++++++ tests/Js/Main/HashSetTests.fs | 17 ++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index bfd354ce76..670def7c51 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -1857,6 +1857,11 @@ module Util = | Some(Fable.Call(baseRef, info, _, _)), _ -> let baseExpr = match com.Options.Language, baseType, baseRef with + // A BCL base like Dictionary is annotated with an interface, so extend the library class it runs as + | TypeScript, Some(Fable.DeclaredType(_, genArgs)), Fable.Import({ Kind = Fable.LibraryImport _ }, _, _) -> + match transformAsExpr com ctx baseRef with + | Expression.Identifier id -> makeGenericTypeAnnotation com ctx genArgs id |> SuperType + | expr -> SuperExpression expr | TypeScript, Some typ, _ -> makeTypeAnnotation com ctx typ |> SuperType | TypeScript, None, Fable.IdentExpr id -> makeTypeAnnotation com ctx id.Type |> SuperType | _ -> transformAsExpr com ctx baseRef |> SuperExpression diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index 18355832d5..ea7e03ae34 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -28,6 +28,12 @@ type Table<'K, 'V when 'K: equality and 'K: not null> () = member _.Dic = dic +type CountingDictionary() = + inherit Dictionary() + + member this.Increment(key: string) = + this.[key] <- (if this.ContainsKey key then this.[key] else 0) + 1 + let tests = testList "Dictionaries" [ testCase "Dictionary KeyValuePattern works" <| fun () -> // See #509 @@ -384,4 +390,16 @@ let tests = coll.Remove(KeyValuePair("D", 3)) |> equal false coll.Remove(KeyValuePair("B", 2)) |> equal true coll.Count |> equal 2 + + + testCase "Dictionary subclasses work" <| fun () -> + let dic = CountingDictionary() + dic.Increment "a" + dic.Increment "a" + dic.Increment "b" + dic.["a"] |> equal 2 + dic.Count |> equal 2 + dic.ContainsKey "b" |> equal true + dic.Remove "b" |> equal true + dic |> Seq.map (fun kv -> kv.Key, kv.Value) |> List.ofSeq |> equal [ "a", 2 ] ] diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index 0ee52339b6..6653ea0e35 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -30,6 +30,12 @@ type RefEqRecord = { a: int; b: int } type MyRecord = { a: int } +type NameSet() = + inherit HashSet() + + member this.AddAll(names: string list) = + names |> List.filter this.Add |> List.length + type R = { i: int; s: string } type SetItem = @@ -416,4 +422,13 @@ let tests = coll.Remove(("D", 3)) |> equal false coll.Remove(("B", 2)) |> equal true coll.Count |> equal 2 - ] \ No newline at end of file + + + testCase "HashSet subclasses work" <| fun () -> + let names = NameSet() + names.AddAll [ "a"; "b"; "a" ] |> equal 2 + names.Count |> equal 2 + names.Contains "b" |> equal true + names.Remove "b" |> equal true + names |> List.ofSeq |> equal [ "a" ] + ] From b2f075b3d1e561feaa96bd8ff6e41eeb10068e45 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 1 Nov 2026 23:12:54 +0100 Subject: [PATCH 293/322] ci: restrict privileged workflows to trusted triggers --- .github/workflows/claude.yml | 18 ++--- .github/workflows/commands.yml | 94 -------------------------- .github/workflows/easybuild-shipit.yml | 5 ++ 3 files changed, 15 insertions(+), 102 deletions(-) delete mode 100644 .github/workflows/commands.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6b2f2be0c8..7ef7a068ca 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,15 +19,17 @@ jobs: name: Claude runs-on: ubuntu-latest - # Only trigger on @claude mentions from trusted contributors - # (NONE = first-time contributors with no prior contributions) + # Only trigger on @claude mentions from maintainers; a review carries its author on `review`, not `comment` if: | - ( - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) - ) && - github.event.comment.author_association != 'NONE' + (github.event_name == 'issue_comment' + && contains(github.event.comment.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review_comment' + && contains(github.event.comment.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) || + (github.event_name == 'pull_request_review' + && contains(github.event.review.body, '@claude') + && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) steps: - name: Checkout repository diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml deleted file mode 100644 index 04a69b8915..0000000000 --- a/.github/workflows/commands.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Commands on PR -on: - issue_comment: - types: [created] - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - run_command: - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/run') - runs-on: ubuntu-20.04 - steps: - - name: Extract command to run - uses: actions/github-script@v9.0.0 - id: command-extractor - with: - result-encoding: string - script: | - if (context.eventName !== "issue_comment") throw "Error: This action only works on issue_comment events."; - - // extract the command to run, allowed characters: a-z, A-Z, digits, hyphen, underscore - const regex = /^\/run ([a-zA-Z\d\-\_]+)/; - command = regex.exec(context.payload.comment.body); - if (command == null) throw "Error: No command found in the trigger phrase."; - - return command[1]; - - name: Get github ref - uses: actions/github-script@v9.0.0 - id: get-pr - with: - script: | - const result = await github.pulls.get({ - pull_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }); - return { "ref": result.data.head.ref, "repository": result.data.head.repo.full_name}; - - name: Checkout repo - uses: actions/checkout@v7.0.1 - with: - repository: ${{ fromJson(steps.get-pr.outputs.result).repository }} - ref: ${{ fromJson(steps.get-pr.outputs.result).ref }} - fetch-depth: 0 - - name: Install dotnet - uses: actions/setup-dotnet@v6 - - name: Install dotnet tools - run: dotnet tool restore - - name: Process fantomas command - if: steps.command-extractor.outputs.result == 'fantomas' - id: fantomas - run: dotnet fantomas . -r - - name: Commit and push changes - if: steps.fantomas.outcome == 'success' || steps.xlf.outcome == 'success' - run: | - git config --local user.name "github-actions[bot]" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -a -m 'Automated command ran: ${{ steps.command-extractor.outputs.result }} - - Co-authored-by: ${{ github.event.comment.user.login }} <${{ github.event.comment.user.id }}+${{ github.event.comment.user.login }}@users.noreply.github.com>' - git push - - name: Post command comment - if: steps.fantomas.outcome == 'success' - uses: actions/github-script@v9.0.0 - with: - script: | - // Probably, there's more universal way of getting outputs, but my gh-actions-fu is not that good. - var output = "" - if ("${{steps.command-extractor.outputs.result}}" == 'fantomas') { - output = "${{steps.fantomas.outputs.result}}" - } else if("${{steps.command-extractor.outputs.result}}" == 'xlf') { - output = "${{steps.xlf.outputs.result}}" - } - const body = `Ran ${{ steps.command-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}\n${output}`; - await github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); - - name: Post command failed comment - if: failure() - uses: actions/github-script@v9.0.0 - with: - script: | - const body = `Failed to run ${{ steps.command-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; - await github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body - }); diff --git a/.github/workflows/easybuild-shipit.yml b/.github/workflows/easybuild-shipit.yml index e84c7213d1..eb35850e3f 100644 --- a/.github/workflows/easybuild-shipit.yml +++ b/.github/workflows/easybuild-shipit.yml @@ -20,8 +20,11 @@ jobs: name: ShipIt - Pull Request runs-on: ubuntu-latest # Only run on the upstream repository, and only if the build workflow succeeded + # The branches filter matches the head branch name, which a fork PR also controls if: >- !github.event.repository.fork + && github.event.workflow_run.event == 'push' + && github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.conclusion == 'success' steps: - uses: actions/checkout@v7.0.1 @@ -45,6 +48,8 @@ jobs: runs-on: ubuntu-latest if: >- !github.event.repository.fork + && github.event.workflow_run.event == 'push' + && github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_commit.message, 'chore: release ') steps: From 1190dfa5e5f12793fc2b3b2d15a24afb8253bce3 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 2 Nov 2026 07:54:32 +0100 Subject: [PATCH 294/322] fix(all): keep reads of imported mutable values in place (#5242) --- src/Fable.Transforms/FableTransforms.fs | 12 +++++++++++- tests/Js/Main/MiscTests.fs | 5 +++++ tests/Js/Main/MiscTestsHelper.fs | 6 ++++++ tests/Python/MiscTestsHelper.fs | 6 ++++++ tests/Python/TestMisc.fs | 6 ++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/FableTransforms.fs b/src/Fable.Transforms/FableTransforms.fs index 907395faff..70730b2074 100644 --- a/src/Fable.Transforms/FableTransforms.fs +++ b/src/Fable.Transforms/FableTransforms.fs @@ -409,11 +409,21 @@ module private Transforms = | _ -> ValueNone let tryInlineBinding (com: Compiler) (ident: Ident) value letBody = + let readsMutableValue callee = + match callee with + | IdentExpr id -> id.IsMutable + | Import({ Kind = MemberImport memberRef }, _, _) -> + com.TryGetMember(memberRef) |> Option.exists (fun memb -> memb.IsMutable) + | _ -> false + let canInlineBinding = match value with | Import(i, _, _) -> i.IsCompilerGenerated | Call(callee, info, _, _) when List.isEmpty info.Args && List.contains "value" info.Tags -> - canInlineArg com ident.Name callee letBody + if readsMutableValue callee then + canInlineArg com ident.Name value letBody + else + canInlineArg com ident.Name callee letBody // Replace non-recursive lambda bindings | NestedLambda(_args, lambdaBody, _name) -> match lambdaBody with diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index 94153e6d16..a695d7b7a1 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -1314,6 +1314,11 @@ let tests = Option.defaultWith (fun () -> invalidOp "This is invalid") (Some 3) |> equal 3 throwsError "This is invalid" (fun () -> Option.defaultWith (fun () -> invalidOp "This is invalid") None) + testCase "A read of another file's mutable value is not moved past side effects" <| fun () -> // See #5156 + let before = MiscTestsHelper.trackedCalls + MiscTestsHelper.track 1 |> ignore + MiscTestsHelper.trackedCalls - before |> equal 1 + testCase "Pattern matching optimization works (switch statement)" <| fun () -> let mutable x = "" let i = 4 diff --git a/tests/Js/Main/MiscTestsHelper.fs b/tests/Js/Main/MiscTestsHelper.fs index 3157415696..3f20c76912 100644 --- a/tests/Js/Main/MiscTestsHelper.fs +++ b/tests/Js/Main/MiscTestsHelper.fs @@ -31,3 +31,9 @@ type Vector2<[] 'u> = Vector2 of x: float<'u> * y: float<'u> with static member inline ( + ) (Vector2(ax, ay), Vector2(bx, by)) = Vector2(ax + bx, ay + by) static member inline ( * ) (scalar, Vector2(x, y)) = Vector2(scalar * x, scalar * y) + +let mutable trackedCalls = 0 + +let track x = + trackedCalls <- trackedCalls + 1 + x diff --git a/tests/Python/MiscTestsHelper.fs b/tests/Python/MiscTestsHelper.fs index 8f4352e620..ae7f2ab643 100644 --- a/tests/Python/MiscTestsHelper.fs +++ b/tests/Python/MiscTestsHelper.fs @@ -42,3 +42,9 @@ type CrossModuleDemo = static member propDefault = A "prop" static member methDefault() = A "meth" + +let mutable trackedCalls = 0 + +let track x = + trackedCalls <- trackedCalls + 1 + x diff --git a/tests/Python/TestMisc.fs b/tests/Python/TestMisc.fs index ab47fc7125..2e7d545f41 100644 --- a/tests/Python/TestMisc.fs +++ b/tests/Python/TestMisc.fs @@ -1833,3 +1833,9 @@ let ``test lock storage releases idle entries after exceptions`` () = with _ -> () equal before storage.count #endif + +[] +let ``test A read of another file's mutable value is not moved past side effects`` () = // See #5156 + let before = MiscTestsHelper.trackedCalls + MiscTestsHelper.track 1 |> ignore + MiscTestsHelper.trackedCalls - before |> equal 1 From 60771e077577fdcdf5e40e82630cf88e8cfad560 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 2 Nov 2026 07:57:28 +0100 Subject: [PATCH 295/322] fix(all): give imported .fsx files their output extension (#5241) --- src/Fable.Cli/Pipeline.fs | 20 +++++++++++++++---- src/Fable.Compiler/Util.fs | 1 + .../Integration/CompilationTests.fs | 5 ++++- .../Integration/data/fsxImportExtension/a.fsx | 1 + .../data/fsxImportExtension/main.fsx | 4 ++++ .../data/fsxImportExtension/main.jsx.expected | 7 +++++++ 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/Integration/Integration/data/fsxImportExtension/a.fsx create mode 100644 tests/Integration/Integration/data/fsxImportExtension/main.fsx create mode 100644 tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected diff --git a/src/Fable.Cli/Pipeline.fs b/src/Fable.Cli/Pipeline.fs index 25166a2abc..5f8721dc7b 100644 --- a/src/Fable.Cli/Pipeline.fs +++ b/src/Fable.Cli/Pipeline.fs @@ -139,7 +139,10 @@ module Js = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then let isInFableModules = Path.Combine(targetDir, path) |> Naming.isInFableModules File.changeExtensionButUseDefaultExtensionInFableModules JavaScript isInFableModules path fileExt @@ -412,7 +415,10 @@ module Php = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path @@ -451,7 +457,10 @@ module Dart = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path @@ -507,7 +516,10 @@ module Rust = let path = Imports.getImportPath pathResolver sourcePath targetPath projDir cliArgs.OutDir path - if path.EndsWith(".fs", StringComparison.Ordinal) then + if + path.EndsWith(".fs", StringComparison.Ordinal) + || path.EndsWith(".fsx", StringComparison.Ordinal) + then Path.ChangeExtension(path, fileExt) else path diff --git a/src/Fable.Compiler/Util.fs b/src/Fable.Compiler/Util.fs index dbc1d9d8fd..c312778ac3 100644 --- a/src/Fable.Compiler/Util.fs +++ b/src/Fable.Compiler/Util.fs @@ -680,6 +680,7 @@ module Imports = if isAbsolutePath importPath then if importPath.EndsWith(".fs", StringComparison.Ordinal) + || importPath.EndsWith(".fsx", StringComparison.Ordinal) || importPath.EndsWith(".rs", StringComparison.Ordinal) then getTargetRelativePath pathResolver importPath targetDir projDir outDir diff --git a/tests/Integration/Integration/CompilationTests.fs b/tests/Integration/Integration/CompilationTests.fs index c6b67745cc..d1bc6070b4 100644 --- a/tests/Integration/Integration/CompilationTests.fs +++ b/tests/Integration/Integration/CompilationTests.fs @@ -63,7 +63,10 @@ let tests = testCaseDir (async { let project = - Directory.GetFileSystemEntries(testCaseDir, "*.fsproj") |> Seq.exactlyOne + match Directory.GetFileSystemEntries(testCaseDir, "*.fsproj") with + | [| project |] -> project + | [||] -> Path.Combine(testCaseDir, "main.fsx") + | projects -> failwith $"Expected one project in {testCaseDir}, found {projects.Length}" // clean up old actual files for f in Directory.GetFileSystemEntries(testCaseDir, "*.actual") do File.Delete f diff --git a/tests/Integration/Integration/data/fsxImportExtension/a.fsx b/tests/Integration/Integration/data/fsxImportExtension/a.fsx new file mode 100644 index 0000000000..f0a06fd5c8 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/a.fsx @@ -0,0 +1 @@ +let a v = v + 1 diff --git a/tests/Integration/Integration/data/fsxImportExtension/main.fsx b/tests/Integration/Integration/data/fsxImportExtension/main.fsx new file mode 100644 index 0000000000..ba10438697 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/main.fsx @@ -0,0 +1,4 @@ +#load "a.fsx" +open A + +let b v = a v + 1 diff --git a/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected b/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected new file mode 100644 index 0000000000..3003d78626 --- /dev/null +++ b/tests/Integration/Integration/data/fsxImportExtension/main.jsx.expected @@ -0,0 +1,7 @@ + +import { a } from "./a.jsx.actual"; + +export function b(v) { + return (a(v) + 1) | 0; +} + From 1d77d6282b8dea284bca3cff9a6455e561575e3a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Mon, 2 Nov 2026 08:31:32 +0100 Subject: [PATCH 296/322] fix(js/ts): match Dictionary/HashSet array keys by reference (#5239) --- src/Fable.Transforms/Replacements.fs | 44 +++++++++++++++++++++------- tests/Js/Main/DictionaryTests.fs | 40 ++++++++++++++++++++++++- tests/Js/Main/HashSetTests.fs | 37 ++++++++++++++++++++++- 3 files changed, 109 insertions(+), 12 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index f49ec20666..7b0d7005c0 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -832,6 +832,23 @@ let makeEqualityComparer (com: ICompiler) ctx typArg = "GetHashCode", Delegate([ x ], structuralHash com None (IdentExpr x), None, Tags.empty) ] +/// Matches EqualityComparer.Default, which compares arrays and List by reference in .NET +let makeDefaultEqualityComparer (com: ICompiler) ctx typArg = + match typArg with + | Array _ -> + let x = makeUniqueIdent com ctx typArg "x" + let y = makeUniqueIdent com ctx typArg "y" + + let hash = + Helper.LibCall(com, "Util", "physicalHash", Int32.Number, [ IdentExpr x ]) + + objExpr + [ + "Equals", Delegate([ x; y ], makeEqOp None (IdentExpr x) (IdentExpr y) BinaryEqual, None, Tags.empty) + "GetHashCode", Delegate([ x ], hash, None, Tags.empty) + ] + | _ -> makeEqualityComparer com ctx typArg + // TODO: Try to detect at compile-time if the object already implements `Compare`? let inline makeComparerFromEqualityComparer e = e // leave it as is, if implementation supports it // Helper.LibCall(com, "Util", "comparerFromEqualityComparer", Any, [e]) @@ -849,13 +866,19 @@ let makeMap (com: ICompiler) ctx r t methName args genArg = let makeDictionaryWithComparer com r t sourceSeq comparer = Helper.LibCall(com, "MutableMap", "Dictionary", t, [ sourceSeq; comparer ], isConstructor = true, ?loc = r) -let makeDictionary (com: ICompiler) ctx r t sourceSeq = +let private makeDictionaryWithKeyComparer makeKeyComparer (com: ICompiler) ctx r t sourceSeq = match t with | DeclaredType(_, [ key; _ ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key - makeEqualityComparer com ctx key |> makeDictionaryWithComparer com r t sourceSeq + makeKeyComparer com ctx key |> makeDictionaryWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Map", t, [ sourceSeq ], isConstructor = true, ?loc = r) +let makeDictionary (com: ICompiler) ctx r t sourceSeq = + makeDictionaryWithKeyComparer makeEqualityComparer com ctx r t sourceSeq + +let makeBclDictionary (com: ICompiler) ctx r t sourceSeq = + makeDictionaryWithKeyComparer makeDefaultEqualityComparer com ctx r t sourceSeq + let makeHashSetWithComparer com r t sourceSeq comparer = Helper.LibCall(com, "MutableSet", "HashSet", t, [ sourceSeq; comparer ], isConstructor = true, ?loc = r) @@ -863,7 +886,8 @@ let makeHashSet (com: ICompiler) ctx r t sourceSeq = match t with | DeclaredType(_, [ key ]) when not (isCompatibleWithNativeComparison com key) -> // makeComparer com ctx key - makeEqualityComparer com ctx key |> makeHashSetWithComparer com r t sourceSeq + makeDefaultEqualityComparer com ctx key + |> makeHashSetWithComparer com r t sourceSeq | _ -> Helper.GlobalCall("Set", t, [ sourceSeq ], isConstructor = true, ?loc = r) let tryEntityIdent (com: Compiler) entFullName = @@ -3222,13 +3246,13 @@ let dictionaries (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Exp match i.CompiledName, thisArg with | ".ctor", _ -> match i.SignatureArgTypes, args with - | ([] | [ Number _ ]), _ -> makeDictionary com ctx r t (makeArray Any []) |> Some - | [ IDictionary ], [ arg ] -> makeDictionary com ctx r t arg |> Some + | ([] | [ Number _ ]), _ -> makeBclDictionary com ctx r t (makeArray Any []) |> Some + | [ IDictionary ], [ arg ] -> makeBclDictionary com ctx r t arg |> Some | [ IDictionary; IEqualityComparer ], [ arg; eqComp ] -> makeComparerFromEqualityComparer eqComp |> makeDictionaryWithComparer com r t arg |> Some - | [ IEnumerable ], [ arg ] -> makeDictionary com ctx r t arg |> Some + | [ IEnumerable ], [ arg ] -> makeBclDictionary com ctx r t arg |> Some | [ IEnumerable; IEqualityComparer ], [ arg; eqComp ] -> makeComparerFromEqualityComparer eqComp |> makeDictionaryWithComparer com r t arg @@ -5208,8 +5232,8 @@ let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genA | Types.dictionary -> let args = match argTypes.Value, args with - | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeEqualityComparer com ctx (Seq.head genArgs) ] - | [ IDictionary ], [ arg ] -> [ arg; makeEqualityComparer com ctx (Seq.head genArgs) ] + | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] + | [ IDictionary ], [ arg ] -> [ arg; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] | [ IDictionary; IEqualityComparer ], [ arg; eqComp ] -> [ arg; makeComparerFromEqualityComparer eqComp ] | [ IEqualityComparer ], [ eqComp ] | [ Number _; IEqualityComparer ], [ _; eqComp ] -> @@ -5222,8 +5246,8 @@ let tryBaseConstructor com ctx (ent: EntityRef) (argTypes: Lazy) genA | Types.hashset -> let args = match argTypes.Value, args with - | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeEqualityComparer com ctx (Seq.head genArgs) ] - | [ IEnumerable ], [ arg ] -> [ arg; makeEqualityComparer com ctx (Seq.head genArgs) ] + | ([] | [ Number _ ]), _ -> [ makeArray Any []; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] + | [ IEnumerable ], [ arg ] -> [ arg; makeDefaultEqualityComparer com ctx (Seq.head genArgs) ] | [ IEnumerable; IEqualityComparer ], [ arg; eqComp ] -> [ arg; makeComparerFromEqualityComparer eqComp ] | [ IEqualityComparer ], [ eqComp ] | [ Number _; IEqualityComparer ], [ _; eqComp ] -> diff --git a/tests/Js/Main/DictionaryTests.fs b/tests/Js/Main/DictionaryTests.fs index ea7e03ae34..bfa3b37b9c 100644 --- a/tests/Js/Main/DictionaryTests.fs +++ b/tests/Js/Main/DictionaryTests.fs @@ -33,6 +33,8 @@ type CountingDictionary() = member this.Increment(key: string) = this.[key] <- (if this.ContainsKey key then this.[key] else 0) + 1 +type ArrayKeyDictionary() = + inherit Dictionary() let tests = testList "Dictionaries" [ @@ -390,7 +392,6 @@ let tests = coll.Remove(KeyValuePair("D", 3)) |> equal false coll.Remove(KeyValuePair("B", 2)) |> equal true coll.Count |> equal 2 - testCase "Dictionary subclasses work" <| fun () -> let dic = CountingDictionary() @@ -402,4 +403,41 @@ let tests = dic.ContainsKey "b" |> equal true dic.Remove "b" |> equal true dic |> Seq.map (fun kv -> kv.Key, kv.Value) |> List.ofSeq |> equal [ "a", 2 ] + + testCase "Dictionary compares array keys by reference" <| fun _ -> // See #5165 + let key = [| 1 |] + let dic = Dictionary() + dic.[key] <- 1 + dic.[[| 1 |]] <- 2 + dic.Count |> equal 2 + dic.ContainsKey key |> equal true + dic.ContainsKey [| 1 |] |> equal false + dic.[key] |> equal 1 + let copy = Dictionary(dic) + copy.Count |> equal 2 + copy.ContainsKey key |> equal true + copy.ContainsKey [| 1 |] |> equal false + + testCase "Dictionary subclasses compare array keys by reference" <| fun _ -> + let key = [| 1 |] + let dic = ArrayKeyDictionary() + dic.[key] <- 1 + dic.ContainsKey key |> equal true + dic.ContainsKey [| 1 |] |> equal false + + testCase "Dictionary compares ResizeArray keys by reference" <| fun _ -> + let key = ResizeArray [ 1 ] + let dic = Dictionary, int>() + dic.[key] <- 1 + dic.[ResizeArray [ 1 ]] <- 2 + dic.Count |> equal 2 + dic.ContainsKey key |> equal true + dic.ContainsKey(ResizeArray [ 1 ]) |> equal false + + testCase "Dictionary with a structural comparer compares array keys by value" <| fun _ -> + let dic = Dictionary(HashIdentity.Structural) + dic.[[| 1 |]] <- 1 + dic.ContainsKey [| 1 |] |> equal true + (dict [ [| 1 |], 1 ]).ContainsKey [| 1 |] |> equal true + (readOnlyDict [ [| 1 |], 1 ]).ContainsKey [| 1 |] |> equal true ] diff --git a/tests/Js/Main/HashSetTests.fs b/tests/Js/Main/HashSetTests.fs index 6653ea0e35..73542abd3c 100644 --- a/tests/Js/Main/HashSetTests.fs +++ b/tests/Js/Main/HashSetTests.fs @@ -28,6 +28,9 @@ let inline hashSet l = [] type RefEqRecord = { a: int; b: int } +type ArrayHashSet() = + inherit HashSet() + type MyRecord = { a: int } type NameSet() = @@ -422,7 +425,6 @@ let tests = coll.Remove(("D", 3)) |> equal false coll.Remove(("B", 2)) |> equal true coll.Count |> equal 2 - testCase "HashSet subclasses work" <| fun () -> let names = NameSet() @@ -431,4 +433,37 @@ let tests = names.Contains "b" |> equal true names.Remove "b" |> equal true names |> List.ofSeq |> equal [ "a" ] + + testCase "HashSet compares arrays by reference" <| fun _ -> // See #5165 + let item = [| 1 |] + let xs = HashSet() + xs.Add item |> equal true + xs.Add item |> equal false + xs.Add [| 1 |] |> equal true + xs.Count |> equal 2 + xs.Contains item |> equal true + xs.Contains [| 1 |] |> equal false + HashSet([ [| 1 |]; [| 1 |] ]).Count |> equal 2 + + testCase "HashSet subclasses compare arrays by reference" <| fun _ -> + let item = [| 1 |] + let xs = ArrayHashSet() + xs.Add item |> equal true + xs.Contains item |> equal true + xs.Contains [| 1 |] |> equal false + + testCase "HashSet compares ResizeArrays by reference" <| fun _ -> + let item = ResizeArray [ 1 ] + let xs = HashSet>() + xs.Add item |> equal true + xs.Add(ResizeArray [ 1 ]) |> equal true + xs.Count |> equal 2 + xs.Contains item |> equal true + xs.Contains(ResizeArray [ 1 ]) |> equal false + + testCase "HashSet with a structural comparer compares arrays by value" <| fun _ -> + let xs = HashSet(HashIdentity.Structural) + xs.Add [| 1 |] |> equal true + xs.Add [| 1 |] |> equal false + xs.Contains [| 1 |] |> equal true ] From 350a390071473bb4e93fb055a3876d7b3215ada1 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 15:37:32 +0100 Subject: [PATCH 297/322] fix(js/ts): round exact midpoints to even in numeric formats (#5245) --- src/fable-library-ts/Numeric.ts | 65 +++++++++++++++++++++++++++++++++ src/fable-library-ts/String.ts | 28 +++++++++----- tests/Js/Main/StringTests.fs | 38 +++++++++++++++++++ 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/fable-library-ts/Numeric.ts b/src/fable-library-ts/Numeric.ts index 1d242ab9f2..19c982d22f 100644 --- a/src/fable-library-ts/Numeric.ts +++ b/src/fable-library-ts/Numeric.ts @@ -109,6 +109,71 @@ export function toExponential(x: Numeric, dp?: number) { } } +function isExactTie(x: number, k: number) { + if (k >= 0) { + const t = x * 2 ** (k + 1); + return Number.isInteger(t) && t % 2 !== 0; + } else if (Number.isInteger(x)) { + const p = 10n ** BigInt(-k); + return BigInt(Math.abs(x)) % p === p / 2n; + } else { + return false; + } +} + +function truncateLastDigit(digits: string) { + const s = digits.slice(0, -1); + return s.endsWith(".") ? s.slice(0, -1) : s; +} + +function isLastDigitEven(digits: string) { + return digits.charCodeAt(digits.length - 1) % 2 === 0; +} + +// .NET standard numeric formats give an exact midpoint of a double the even digit +export function toFixedHalfEven(x: Numeric, dp: number) { + if (typeof x === "number" && dp < 100 && isExactTie(x, dp)) { + const truncated = truncateLastDigit(x.toFixed(dp + 1)); + return isLastDigitEven(truncated) ? truncated : x.toFixed(dp); + } else { + return toFixed(x, dp); + } +} + +export function toExponentialHalfEven(x: Numeric, dp: number) { + if (typeof x === "number" && Number.isFinite(x) && dp < 100) { + const exact = x.toExponential(dp + 1); + const eIdx = exact.indexOf("e"); + const mantissa = exact.slice(0, eIdx); + if (mantissa.endsWith("5") && isExactTie(x, dp - Number(exact.slice(eIdx + 1)))) { + const truncated = truncateLastDigit(mantissa); + return isLastDigitEven(truncated) ? truncated + exact.slice(eIdx) : x.toExponential(dp); + } + } + return toExponential(x, dp); +} + +export function toPrecisionHalfEven(x: Numeric, sd: number) { + if (typeof x !== "number" || !(sd > 0)) { + return toPrecision(x, sd); + } + const s = toExponentialHalfEven(x, sd - 1); + if (s === x.toExponential(sd - 1)) { + return x.toPrecision(sd); + } + // Same notation as Number.prototype.toPrecision + const eIdx = s.indexOf("e"); + const e = Number(s.slice(eIdx + 1)); + if (e < -6 || e >= sd) { + return s; + } + const sign = s.startsWith("-") ? "-" : ""; + const digits = s.slice(sign.length, eIdx).replace(".", ""); + return e < 0 + ? sign + "0." + "0".repeat(-e - 1) + digits + : sign + digits.slice(0, e + 1) + (sd > e + 1 ? "." + digits.slice(e + 1) : ""); +} + export function toRadix(x: Numeric, radix: number) { if (typeof x === "number") { return (Number(x) >>> 0).toString(radix); diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index 4213448d65..b6120c2883 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -1,5 +1,5 @@ import { toString as dateToString } from "./Date.ts"; -import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toFixed, toHex, toPrecision, toRadix } from "./Numeric.ts"; +import { compare as numericCompare, divide, isNumeric, isIntegral, multiply, Numeric, toExponential, toExponentialHalfEven, toFixed, toFixedHalfEven, toHex, toPrecision, toPrecisionHalfEven, toRadix } from "./Numeric.ts"; import { escape } from "./RegExp.ts"; import { ArgumentNullException_$ctor_Z721C83C5, ArgumentOutOfRangeException_$ctor_Z384F8060, ArgumentOutOfRangeException_$ctor_Z721C83C5, FormatException_$ctor_Z721C83C5 } from "./System.ts"; import { toString } from "./Types.ts"; @@ -245,13 +245,13 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, switch (format) { case "f": case "F": precision = precision != null ? precision : 6; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); break; case "g": case "G": - rep = precision != null ? toPrecision(rep, precision) : toPrecision(rep); + rep = precision != null ? toPrecisionHalfEven(rep, precision) : toPrecision(rep); break; case "e": case "E": - rep = precision != null ? toExponential(rep, precision) : toExponential(rep); + rep = precision != null ? toExponentialHalfEven(rep, precision) : toExponential(rep); break; case "x": case "o": rep = toRadix(rep, radix); @@ -362,6 +362,14 @@ function splitIntAndDecimalPart(value: string) { } } +// .NET rounds a percentage on the unscaled value, at precision + 2 decimals +function shiftPercent(value: string) { + const sign = value.startsWith("-") ? "-" : ""; + const [integral, decimal = ""] = value.slice(sign.length).split("."); + const shifted = (integral + decimal.slice(0, 2)).replace(/^0+(?=\d)/, ""); + return sign + shifted + (decimal.length > 2 ? "." + decimal.slice(2) : ""); +} + function thousandSeparate(value: string) { return value.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } @@ -397,7 +405,7 @@ function formatGeneral(x: number, format: string, precision: number | null, isSi // R ignores the precision and G0 is G const p = format === "r" || format === "R" ? 0 : precision ?? 0; const [significand, exponent] = - p > 0 ? splitExponential(x.toExponential(p - 1)) : isSingle ? shortestSingle(x) : splitExponential(x.toExponential()); + p > 0 ? splitExponential(toExponentialHalfEven(x, p - 1)) : isSingle ? shortestSingle(x) : splitExponential(x.toExponential()); const digits = trimEnd(significand, "0") || "0"; if (exponent < -4 || exponent >= (p > 0 ? p : isSingle ? 9 : 17)) { const mantissa = digits.length > 1 ? digits[0] + "." + digits.slice(1) : digits; @@ -450,7 +458,7 @@ export function format(str: string | object, ...args: any[]) { rep = multiply(rep, -1); } precision = precision == null ? 2 : precision; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = "¤" + thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); @@ -476,7 +484,7 @@ export function format(str: string | object, ...args: any[]) { break; case "e": case "E": { precision = precision != null ? precision : 6; - rep = String(toExponential(rep, precision)); + rep = toExponentialHalfEven(rep, precision); // .NET always signs the exponent and pads it to at least three digits const eIdx = rep.indexOf("e"); rep = rep.slice(0, eIdx) + format + rep[eIdx + 1] + padLeft(rep.slice(eIdx + 2), 3, "0"); @@ -484,7 +492,7 @@ export function format(str: string | object, ...args: any[]) { } case "f": case "F": precision = precision != null ? precision : 2; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); if (precision > 0) { parts = splitIntAndDecimalPart(rep); rep = parts.integral + "." + padRight(parts.decimal, precision, "0"); @@ -518,7 +526,7 @@ export function format(str: string | object, ...args: any[]) { } case "n": case "N": precision = precision != null ? precision : 2; - rep = toFixed(rep, precision); + rep = toFixedHalfEven(rep, precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0"); @@ -528,7 +536,7 @@ export function format(str: string | object, ...args: any[]) { break; case "p": case "P": precision = precision != null ? precision : 2; - rep = toFixed(multiply(rep, 100), precision) + rep = typeof rep === "number" ? shiftPercent(toFixedHalfEven(rep, precision + 2)) : toFixed(multiply(rep, 100), precision); parts = splitIntAndDecimalPart(rep); if (precision > 0) { rep = thousandSeparate(parts.integral) + "." + padRight(parts.decimal, precision, "0") + " %"; diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index f781bae0e5..a12c20e32c 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -591,6 +591,44 @@ let tests = testList "Strings" [ String.Format(CultureInfo.InvariantCulture, "{0:P2}", 0.1234) |> equal "12.34 %" String.Format(CultureInfo.InvariantCulture, "{0:C2}", 1000) |> equal "¤1,000.00" + testCase "sprintf rounds exact midpoints of floats to even" <| fun () -> // See #5202 + sprintf "%.2f" 0.125 |> equal "0.12" + sprintf "%.0f" 2.5 |> equal "2" + sprintf "%.0f" 0.5 |> equal "0" + sprintf "%.0f" 1.5 |> equal "2" + sprintf "%.0f" -2.5 |> equal "-2" + sprintf "%.0f" -0.5 |> equal "-0" + sprintf "%.0f" 2.5f |> equal "2" + sprintf "%.2g" 0.125 |> equal "0.12" + sprintf "%.2f" 1.005 |> equal "1.00" + sprintf "%.2f" 0.375 |> equal "0.38" + sprintf "%.1f" 0.05 |> equal "0.1" + sprintf "%.0f" 2.5m |> equal "3" + sprintf "%.2f" 0.125m |> equal "0.13" + + testCase "Standard numeric formats round exact midpoints of floats to even" <| fun () -> // See #5202 + (0.125).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.12" + (2.5).ToString("F0", CultureInfo.InvariantCulture) |> equal "2" + (-2.5).ToString("F0", CultureInfo.InvariantCulture) |> equal "-2" + (0.125f).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.12" + (0.125).ToString("E1", CultureInfo.InvariantCulture) |> equal "1.2E-001" + (9.5).ToString("E0", CultureInfo.InvariantCulture) |> equal "1E+001" + (2.5e20).ToString("E0", CultureInfo.InvariantCulture) |> equal "2E+020" + (3.5e20).ToString("E0", CultureInfo.InvariantCulture) |> equal "4E+020" + (0.125).ToString("P0", CultureInfo.InvariantCulture) |> equal "12 %" + (-0.125).ToString("P0", CultureInfo.InvariantCulture) |> equal "-12 %" + (0.00125).ToString("P1", CultureInfo.InvariantCulture) |> equal "0.1 %" + (0.005).ToString("P0", CultureInfo.InvariantCulture) |> equal "1 %" + (2.5).ToString("N0", CultureInfo.InvariantCulture) |> equal "2" + (2.5).ToString("C0", CultureInfo.InvariantCulture) |> equal "¤2" + (0.125).ToString("G2", CultureInfo.InvariantCulture) |> equal "0.12" + String.Format(CultureInfo.InvariantCulture, "{0:F2}", 0.125) |> equal "0.12" + (1.005).ToString("F2", CultureInfo.InvariantCulture) |> equal "1.00" + (0.375).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.38" + (0.125).ToString("0.00", CultureInfo.InvariantCulture) |> equal "0.13" + (2.5m).ToString("F0", CultureInfo.InvariantCulture) |> equal "3" + (0.125m).ToString("F2", CultureInfo.InvariantCulture) |> equal "0.13" + testCase "G format specifier trims trailing zeros without corrupting exponential notation" <| fun () -> (123.45).ToString("G17", CultureInfo.InvariantCulture) |> equal "123.45" (10000.0).ToString("G17", CultureInfo.InvariantCulture) |> equal "10000" From 8014fb20fed77653aa8f119cfb080d86b59ae408 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 16:35:10 +0100 Subject: [PATCH 298/322] fix(js/ts): round Math.Round ties on the exact value (#5254) --- src/Fable.Transforms/Replacements.fs | 2 ++ src/fable-library-ts/Util.ts | 12 +++++------- tests/Js/Main/ArithmeticTests.fs | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 7b0d7005c0..f352f378c2 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -492,6 +492,8 @@ let roundCall (com: ICompiler) r t moduleName (i: CallInfo) (args: Expr list) = | [ ExprType(Number(Float32, _)); _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundToModeFloat32", t, args, ?loc = r) | [ _; _; _ ] when isMidpointRounding -> Helper.LibCall(com, moduleName, "roundToMode", t, args, ?loc = r) + | [ ExprType(Number(Float32, _)); _ ] -> + Helper.LibCall(com, moduleName, "roundFloat32", t, args, i.SignatureArgTypes, ?loc = r) | _ -> Helper.LibCall(com, moduleName, "round", t, args, i.SignatureArgTypes, ?loc = r) let toList com returnType expr = diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 343b8f2283..484846d3e7 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -705,13 +705,11 @@ export function jsOptions(mutator: (x: T) => void): T { } export function round(value: number, digits: number = 0) { - const m = Math.pow(10, digits); - const n = +(digits ? value * m : value).toFixed(8); - const i = Math.floor(n); - const f = n - i; - const e = 1e-8; - const r = (f > 0.5 - e && f < 0.5 + e) ? ((i % 2 === 0) ? i : i + 1) : Math.round(n); - return digits ? r / m : r; + return roundToMode(value, digits, MidpointRounding.ToEven); +} + +export function roundFloat32(value: number, digits: number = 0) { + return roundToModeFloat32(value, digits, MidpointRounding.ToEven); } export const MidpointRounding = { diff --git a/tests/Js/Main/ArithmeticTests.fs b/tests/Js/Main/ArithmeticTests.fs index 4848dcbf49..dc94da202f 100644 --- a/tests/Js/Main/ArithmeticTests.fs +++ b/tests/Js/Main/ArithmeticTests.fs @@ -903,6 +903,34 @@ let tests = Math.Round(-1.426, 2) |> equal -1.43 Math.Round(-1.426, 1) |> equal -1.4 + testCase "Math.Round rounds the exact value" <| fun () -> // See #5251 + Math.Round 2.500000001 |> equal 3. + Math.Round 3.4999999999 |> equal 3. + Math.Round -2.500000001 |> equal -3. + Math.Round -3.4999999999 |> equal -3. + Math.Round 2.5 |> equal 2. + Math.Round 3.5 |> equal 4. + Math.Round 0.49999999999999994 |> equal 0. + 1. / Math.Round(-0.5) |> equal -infinity + Math.Round(1.2500000001, 1) |> equal 1.3 + Math.Round(2.0000000050001, 8) |> equal 2.00000001 + Math.Round(2.675, 2) |> equal 2.68 + Math.Round(123456.785, 2) |> equal 123456.78 + round 2.500000001 |> equal 3. + round 3.4999999999 |> equal 3. + Convert.ToInt32 2.500000001 |> equal 3 + Convert.ToInt32 3.4999999999 |> equal 3 + MathF.Round 2.500001f |> equal 3.f + MathF.Round 3.499999f |> equal 3.f + MathF.Round 2.5f |> equal 2.f + + testCase "Math.Round validates its digits" <| fun () -> + (fun () -> Math.Round(1., 16)) |> throwsAnyError + (fun () -> Math.Round(1., -1)) |> throwsAnyError + (fun () -> MathF.Round(1.f, 7)) |> throwsAnyError + Math.Round(1.2345678901234567, 15) |> equal 1.234567890123457 + Math.Round(1e16, 2) |> equal 1e16 + testCase "Math.Round with MidpointRounding works" <| fun () -> let round mode = [ 2.5; -2.5; 3.5; -3.5; 2.4; -2.6; 0.5; -0.5 ] |> List.map (fun x -> Math.Round(x, (mode: MidpointRounding))) round MidpointRounding.ToEven |> equal [ 2.; -2.; 4.; -4.; 2.; -3.; 0.; -0. ] From 1e41dcc5ac5a990a81aa15d43921d824b5466740 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 16:35:41 +0100 Subject: [PATCH 299/322] fix(all): rename idents of a witness inlined twice (#5249) --- src/Fable.Transforms/FSharp2Fable.Util.fs | 30 +++++++++++++ src/Fable.Transforms/FSharp2Fable.fs | 4 +- tests/Js/Main/ApplicativeTests.fs | 50 ++++++++++++++++++++++ tests/Python/TestApplicative.fs | 51 +++++++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 9fcd034fa6..b9ff3043a9 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1998,6 +1998,36 @@ module Identifiers = let tryGetIdentFromScope (ctx: Context) r typ (fsRef: FSharpMemberOrFunctionOrValue) = tryGetIdentFromScopeIf ctx r typ fsRef.Equals + let getDeclaredIdentNames (expr: Fable.Expr) = + let names = HashSet() + + let rec loop (e: Fable.Expr) = + let declared: Fable.Ident list = + match e with + | Fable.Let(i, _, _) + | Fable.Lambda(i, _, _) + | Fable.ForLoop(i, _, _, _, _, _) + | Fable.TryCatch(_, Some(i, _), _, _) -> [ i ] + | Fable.LetRec(bindings, _) -> List.map fst bindings + | Fable.Delegate(args, _, _, _) -> args + | Fable.DecisionTree(_, targets) -> List.collect fst targets + | Fable.ObjectExpr(members, _, _) -> List.collect (fun (m: Fable.ObjectExprMember) -> m.Args) members + | _ -> [] + + for i in declared do + names.Add(i.Name) |> ignore + + match e with + | Fable.Unresolved(Fable.UnresolvedTraitCall(argExprs = argExprs), _, _) -> argExprs + | Fable.Unresolved(Fable.UnresolvedInlineCall(_, _, callee, info), _, _) -> + Option.toList callee @ Option.toList info.ThisArg @ info.Args + | Fable.Unresolved(Fable.UnresolvedReplaceCall(thisArg, args, _, _), _, _) -> Option.toList thisArg @ args + | e -> getSubExpressions e + |> List.iter loop + + loop expr + set names + module Util = open Helpers open Patterns diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 46edbf930e..724142ae23 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2875,13 +2875,15 @@ let resolveInlineExpr (com: IFableCompiler) ctx info expr = | None -> transformTraitCall com ctx r t sourceTypes traitName isInstance argTypes argExprs | Some w -> // As witnesses come from the context, idents may be duplicated, see #2855 + // The same witness can be inlined several times in one scope, see #3921 let info = { info with + ScopeIdents = Set.union info.ScopeIdents (getDeclaredIdentNames w.Expr) ResolvedIdents = Dictionary() FileName = w.FileName } - let callee = resolveInlineExpr com ctx { info with FileName = w.FileName } w.Expr + let callee = resolveInlineExpr com ctx info w.Expr let callInfo = makeCallInfo None argExprs argTypes makeCall r t callInfo callee diff --git a/tests/Js/Main/ApplicativeTests.fs b/tests/Js/Main/ApplicativeTests.fs index 4916bed08e..1077f7f75a 100644 --- a/tests/Js/Main/ApplicativeTests.fs +++ b/tests/Js/Main/ApplicativeTests.fs @@ -1727,6 +1727,53 @@ module MultipleInlines = let private mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + let tests = [ testCase "Trait call works with multiple inlined functions I" <| fun () -> // See #2809 let account = Some { Details = { Tag = "Test"; InsurancePolicyData = None }} @@ -1748,6 +1795,9 @@ module MultipleInlines = testCase "Identifiers from witnesses don't get duplicated when resolving inline expressions" <| fun () -> // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) + + testCase "Identifiers from a witness inlined twice in the same scope don't get duplicated" <| fun () -> // See #3921 + getContainerCodecs () |> equal (11, 21) ] module AccessorFunctionShorthand = diff --git a/tests/Python/TestApplicative.fs b/tests/Python/TestApplicative.fs index af0164b7c2..a3d636ca8b 100644 --- a/tests/Python/TestApplicative.fs +++ b/tests/Python/TestApplicative.fs @@ -1659,6 +1659,53 @@ module MultipleInlines = let mapMyList (xs: NonEmptyList) : NonEmptyList = map (fun s -> s + "_") xs + let mutable codecCalls = 0 + + type Codec<'a> = Codec of (unit -> int) + + type Codecs = + static member inline Codecs(_output: int, _mthd: Codecs) : Codec = Codec(fun () -> 0) + static member inline Codecs(_output: bool, _mthd: Codecs) : Codec = Codec(fun () -> 0) + + static member inline Invoke() = + let inline call_2 (a: ^a, b: ^b) = + ((^a or ^b): (static member Codecs: _ * _ -> _) b, a) + + let inline call (a: 'a) = + call_2 (a, Unchecked.defaultof<'r>): Codec<'r> + + call Unchecked.defaultof + + type Codecs with + static member inline Codecs< ^a, ^b + when (^a): (static member codecInner: Codec< ^b > -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + (_output: ^a, _mthd: Codecs) + : Codec< ^a > = + codecCalls <- codecCalls + 1 + let x = codecCalls * 10 + let (Codec inner) = 'a.codecInner (Codecs.Invoke()) + Codec(fun () -> x + inner ()) + + let inline getCodecs<'a, 'b + when (Codecs or ^a): (static member Codecs: ^a * Codecs -> Codec< ^a >) + and (Codecs or ^b): (static member Codecs: ^b * Codecs -> Codec< ^b >)> + () + : Codec< ^a > * Codec< ^b > = + let a: Codec<'a> = Codecs.Invoke() + let b: Codec<'b> = Codecs.Invoke() + (a, b) + + type CodecContainer<'T> = + | CodecContainer + + static member codecInner(Codec f: Codec<'T>) : Codec> = Codec(fun () -> f () + 1) + + let getContainerCodecs () = + codecCalls <- 0 + let (Codec a), (Codec b) = (getCodecs () : Codec> * Codec>) + a (), b () + open MultipleInlines [] @@ -1685,6 +1732,10 @@ let ``test Trait call works with multiple inlined functions II`` () = // See #28 let ``test Identifiers from witnesses don't get duplicated when resolving inline expressions`` () = // See #2855 NonEmptyList("a", ["b"; "c"]) |> mapMyList |> equal (NonEmptyList("a_", ["b_"; "c_"])) +[] +let ``test Identifiers from a witness inlined twice in the same scope don't get duplicated`` () = // See #3921 + getContainerCodecs () |> equal (11, 21) + module AccessorFunctionShorthand = type User = From 1072231c4c64467547d7f69ab741824f2e1a522c Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 16:39:51 +0100 Subject: [PATCH 300/322] fix(js/ts): escape */ in JSDoc comments (#5255) --- src/Fable.Transforms/Babel/BabelPrinter.fs | 2 +- tests/Js/Main/MiscTests.fs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Babel/BabelPrinter.fs b/src/Fable.Transforms/Babel/BabelPrinter.fs index 9bf515eef9..6a5ee77f13 100644 --- a/src/Fable.Transforms/Babel/BabelPrinter.fs +++ b/src/Fable.Transforms/Babel/BabelPrinter.fs @@ -696,7 +696,7 @@ module PrinterExtensions = printer.PrintNewLine() for line in lines do - let line = Naming.xmlDecode line + let line = (Naming.xmlDecode line).Replace("*/", "*\\/") printer.Print(" * ") printer.Print(line.Trim()) diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index a695d7b7a1..a8e5163f7e 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -517,6 +517,21 @@ let paramName (f: unit -> unit) = "no exception" with :? System.ArgumentException as e -> e.ParamName +/// `~` and `^` are *active*/superscript characters, see #5252. +let docCommentWithCommentEnd (c: char) = string c + +/// Ends a block comment */ and opens one /* with a trailing * +type DocCommentRecord = + { + /// Field doc */ + Value: int + } + +/// Class doc */ +type DocCommentClass() = + /// Member doc */ + member _.Get() = 1 + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -1580,4 +1595,9 @@ let tests = sw.IsRunning |> equal true sw.ElapsedMilliseconds < 50L |> equal true } + + testCase "Doc comments containing */ do not break the output" <| fun () -> // See #5252 + docCommentWithCommentEnd 'x' |> equal "x" + { Value = 2 }.Value |> equal 2 + DocCommentClass().Get() |> equal 1 ] From c958156c9f5eb631d78ac331e93c4cfa85e0962a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 17:04:59 +0100 Subject: [PATCH 301/322] fix(js/ts): write floats like .NET's ToString (#5246) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 16 ++++++++++- src/Fable.Transforms/Replacements.fs | 7 ++++- src/fable-library-ts/String.ts | 16 ++++++++++- tests/Js/Main/ConvertTests.fs | 33 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 670def7c51..7e00eb84ef 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -1756,7 +1756,21 @@ module Util = | Fable.StringTemplate(tag, parts, values) -> let tag = tag |> Option.map (fun e -> com.TransformAsExpr(ctx, e)) - let values = values |> List.mapToArray (fun e -> com.TransformAsExpr(ctx, e)) + let values = + values + |> List.mapToArray (fun e -> + let uncasted = + match e with + | Fable.TypeCast(e, _) -> e + | e -> e + + match tag, uncasted.Type with + | None, Fable.Number(Float64, _) -> + libCall com ctx None "String" "float64ToString" [] [ com.TransformAsExpr(ctx, uncasted) ] + | None, Fable.Number(Float32, _) -> + libCall com ctx None "String" "float32ToString" [] [ com.TransformAsExpr(ctx, uncasted) ] + | _ -> com.TransformAsExpr(ctx, e) + ) StringTemplate(tag, List.toArray parts, values, r) |> Literal | Fable.NumberConstant(v, _) -> diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index f352f378c2..fae3830dc3 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -451,6 +451,8 @@ let private toStringWith nullAsEmpty com (ctx: Context) r (args: Expr list) = | Number(NativeInt, _) -> Helper.LibCall(com, "Util", "int64ToString", String, args) | Number(BigInt, _) -> Helper.LibCall(com, "BigInt", "toString", String, args) | Number(Decimal, _) -> Helper.LibCall(com, "Decimal", "toString", String, args) + | Number(Float64, _) -> Helper.LibCall(com, "String", "float64ToString", String, [ head ]) + | Number(Float32, _) -> Helper.LibCall(com, "String", "float32ToString", String, [ head ]) | Number _ -> Helper.InstanceCall(head, "toString", String, tail) | Array _ | List _ -> Helper.LibCall(com, "Types", "seqToString", String, [ head ], ?loc = r) @@ -2962,7 +2964,10 @@ let parseNum (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | "ToString", [ ExprTypeAs(String, format) ] | "ToString", [ ExprTypeAs(String, format); _ (* Culture info *) ] -> numericStringFormat com ctx r t i thisArg format - | "ToString", _ -> Helper.GlobalCall("String", String, [ thisArg.Value ], ?loc = r) |> Some + | "ToString", _ -> + match thisArg.Value.Type with + | Number((Float32 | Float64), _) -> toString com ctx r [ thisArg.Value ] |> Some + | _ -> Helper.GlobalCall("String", String, [ thisArg.Value ], ?loc = r) |> Some | ("Compare" | "CompareTo" | "Equals" | "GetHashCode"), _ -> valueTypes com ctx r t i thisArg args | _ -> None diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index b6120c2883..e3229e73ec 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -259,7 +259,11 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any, case "X": rep = toRadix(rep, radix).toUpperCase(); break; - default: // AOid + case "": + case "O": + rep = typeof rep === "number" ? formatGeneral(rep, "R", null) : String(rep); + break; + default: // Aid rep = String(rep); break; } @@ -424,6 +428,14 @@ export function formatSingle(str: string, x: number) { return m != null ? formatGeneral(Math.fround(x), m[1], m[2] ? parseInt(m[2], 10) : null, true) : format(str, x); } +export function float64ToString(x: number) { + return formatGeneral(x, "R", null); +} + +export function float32ToString(x: number) { + return formatGeneral(Math.fround(x), "R", null, true); +} + export function format(str: string | object, ...args: any[]) { let str2: string; if (typeof str === "object") { @@ -614,6 +626,8 @@ export function format(str: string | object, ...args: any[]) { return repDecimal.length > 0 ? repInt + "." + repDecimal : repInt; }); rep = sign + rep; + } else if (typeof rep === "number") { + rep = formatGeneral(rep, "R", null); } } } else if (rep instanceof Date || isNetDateFormattable(rep)) { diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index b041395469..8bdfc1bdd7 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -1466,6 +1466,39 @@ let tests = let format = "R" (1e-7).ToString(format, CultureInfo.InvariantCulture) |> equal "1E-07" + testCase "float.ToString() chooses the notation like .NET" + <| fun () -> + let values = [ negZero; 1e-4; 1.5e-5; 1e-7; 1e16; 1e17; 123456789012345678901.0; 1e21; Double.Epsilon ] + + let expected = + [ + "-0" + "0.0001" + "1.5E-05" + "1E-07" + "10000000000000000" + "1E+17" + "1.2345678901234568E+20" + "1E+21" + "5E-324" + ] + + values |> List.map string |> equal expected + values |> List.map (fun x -> x.ToString()) |> equal expected + values |> List.map (fun x -> $"{x}") |> equal expected + values |> List.map (fun x -> String.Format("{0}", x)) |> equal expected + values |> List.map (sprintf "%O") |> equal expected + $"%.1f{2.5} {1e17}" |> equal "2.5 1E+17" + String.Format("{0,6}", negZero) |> equal " -0" + + testCase "float32.ToString() chooses the digits and notation like .NET" + <| fun () -> + let values = [ float32 negZero; 1.5e-5f; 3.14f; 1e8f; 1e15f ] + let expected = [ "-0"; "1.5E-05"; "3.14"; "100000000"; "1E+15" ] + values |> List.map string |> equal expected + values |> List.map (fun x -> x.ToString()) |> equal expected + values |> List.map (fun x -> $"{x}") |> equal expected + testCase "byte.ToString 'X' works" <| fun () -> (255uy).ToString("X2") |> equal "FF" (255uy).ToString("X15") |> equal "0000000000000FF" From 03712db9d22e3aa685ec9181aea3ee5c90bb7303 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 17:06:13 +0100 Subject: [PATCH 302/322] fix(js/ts): keep format of interpolated StringFormat (#5248) --- src/Fable.Transforms/Replacements.fs | 44 +++++++++++++++++++++------- src/fable-library-ts/String.ts | 7 +++++ tests/Js/Main/StringTests.fs | 11 +++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index fae3830dc3..93d5dec00a 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1518,8 +1518,23 @@ let (|PrintfNoFormatSpecifiers|_|) (arg: Expr) = | _ -> None | _ -> None +let (|InterpolatedPrintf|_|) (arg: Expr) = + match arg with + | MaybeCasted(Call(Import({ Selector = "interpolatedPrintf" }, _, _), callInfo, _, _)) -> + match callInfo.Args with + | [ _; text ] -> Some text + | _ -> None + | _ -> None + let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr option) (args: Expr list) = + let unwrapInterpolated = + function + | InterpolatedPrintf text -> text + | arg -> arg + let consoleCall consoleMember libMember args = + let args = List.map unwrapInterpolated args + match args with | [ PrintfNoFormatSpecifiers str ] -> Helper.GlobalCall("console", t, [ makeStrConst str ], memb = consoleMember, ?loc = r) @@ -1535,10 +1550,11 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | [ _ ] -> Helper.LibCall(com, "String", "toText", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | [ cont; InterpolatedPrintf text ] -> curriedApply r t cont [ text ] |> Some | [ cont; fmt ] -> Helper.InstanceCall(fmt, "cont", t, [ cont ]) |> Some | _ -> None | "PrintFormatToString", _, _ -> - match args with + match List.map unwrapInterpolated args with | [ template ] when template.Type = String -> Some template | _ -> Helper.LibCall(com, "String", "toText", t, args, i.SignatureArgTypes, ?loc = r) @@ -1551,25 +1567,31 @@ let fsFormat (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op | "PrintFormat", _, _ -> // addWarning com ctx.FileName r "Printf will behave as printfn" consoleCall "log" "toConsole" args + | "PrintFormatThen", _, arg :: InterpolatedPrintf text :: _ -> curriedApply r t arg [ text ] |> Some | "PrintFormatThen", _, arg :: callee :: _ -> Helper.InstanceCall(callee, "cont", t, [ arg ]) |> Some | "PrintFormatToStringThenFail", _, _ -> + let args = List.map unwrapInterpolated args + Helper.LibCall(com, "String", "toFail", t, args, i.SignatureArgTypes, ?loc = r) |> Some | ("PrintFormatToStringBuilder" | "PrintFormatToStringBuilderThen"), // Printf.kbprintf _, _ -> fsharpModule com ctx r t i thisArg args | ".ctor", _, str :: (Value(NewArray(ArrayValues templateArgs, _, _), _) as values) :: _ -> - match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with - | Some v -> makeValue r v |> Some - | None -> - // Try to build a StringTemplate where formatted values are individually wrapped - // in String.interpolate calls. This keeps the AST as a StringTemplate (rather than - // a single runtime interpolate call) so that JSX templates can still use it. - match makeStringTemplateFromAllowingFormat com [| "%s"; "%i" |] templateArgs str with - | Some v -> makeValue r v |> Some + let text = + match makeStringTemplateFrom com [| "%s"; "%i" |] templateArgs str with + | Some v -> makeValue r v | None -> - Helper.LibCall(com, "String", "interpolate", t, [ str; values ], i.SignatureArgTypes, ?loc = r) - |> Some + // Try to build a StringTemplate where formatted values are individually wrapped + // in String.interpolate calls. This keeps the AST as a StringTemplate (rather than + // a single runtime interpolate call) so that JSX templates can still use it. + match makeStringTemplateFromAllowingFormat com [| "%s"; "%i" |] templateArgs str with + | Some v -> makeValue r v + | None -> + Helper.LibCall(com, "String", "interpolate", String, [ str; values ], i.SignatureArgTypes, ?loc = r) + + Helper.LibCall(com, "String", "interpolatedPrintf", t, [ str; text ], ?loc = r) + |> Some | ".ctor", _, arg :: _ -> Helper.LibCall(com, "String", "printf", t, [ arg ], i.SignatureArgTypes, ?loc = r) |> Some diff --git a/src/fable-library-ts/String.ts b/src/fable-library-ts/String.ts index e3229e73ec..3fb987499a 100644 --- a/src/fable-library-ts/String.ts +++ b/src/fable-library-ts/String.ts @@ -180,6 +180,13 @@ export function printf(input: string): IPrintfFormat { }; } +export function interpolatedPrintf(input: string, text: string): IPrintfFormat { + return { + input, + cont: (f: (x: string) => any) => f(text), + }; +} + export function interpolate(str: string, values: any[]): string { let valIdx = 0; let strIdx = 0; diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index a12c20e32c..b1a3f38038 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -277,6 +277,17 @@ let tests = testList "Strings" [ equal "/hello/%s" (pathScan "/hello/%s") + testCase "input of interpolated print format can be retrieved" <| fun () -> // See #3969 + let f (fmt: Printf.StringFormat) = fmt.Value, sprintf fmt + let x, s = 42, "z" + f $"wow %d{x}" |> equal ("wow %d%P()", "wow 42") + f $"a %d{x} b %s{s} c {x + 1}" |> equal ("a %d%P() b %s%P() c %P()", "a 42 b z c 43") + + testCase "ksprintf and kprintf work with interpolated strings" <| fun () -> // See #3969 + let x = 3 + Printf.ksprintf (fun s -> s + "!") $"ks %d{x} {x + 1}" |> equal "ks 3 4!" + Printf.kprintf (fun s -> s + "?") $"kp {x}" |> equal "kp 3?" + testCase "sprintf with escaped percent symbols works" <| fun () -> // See #195 let r, r1, r2 = "Ratio", 0.213849, 0.799898 sprintf "%s1: %.2f%% %s2: %.2f%%" r (r1*100.) r (r2*100.) From ce7fc717c7597b133dc6f999f88a15b2fb636d65 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 17:30:34 +0100 Subject: [PATCH 303/322] fix(js/ts): write negated integer zero without a sign (#5256) --- src/Fable.Transforms/Replacements.fs | 20 +++++++++++++++++++- tests/Js/Main/ConvertTests.fs | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 93d5dec00a..a59ee3112e 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1901,6 +1901,17 @@ let chars (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr optio | ("Compare" | "CompareTo" | "Equals" | "GetHashCode") -> valueTypes com ctx r t i thisArg args | _ -> None +// JS can hold an int as -0, which runtime number formatting would print with its sign +let rec private normalizeIntegerFormatArg (arg: Expr) = + match arg with + | TypeCast(e, t) -> TypeCast(normalizeIntegerFormatArg e, t) + | Value(NewArray(ArrayValues values, typ, kind), r) -> + Value(NewArray(ArrayValues(List.map normalizeIntegerFormatArg values), typ, kind), r) + | e -> + match e.Type with + | Number(Integers _, _) -> makeBinOp None e.Type e (makeIntConst 0) BinaryPlus + | _ -> e + let implementedStringFunctions = set [| @@ -2123,6 +2134,11 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> addErrorAndReturnNull com ctx.InlinePath r |> Some | Patterns.SetContains implementedStringFunctions, thisArg, args -> + let args = + match i.CompiledName, args with + | "Format", format :: formatArgs -> format :: List.map normalizeIntegerFormatArg formatArgs + | _ -> args + Helper.LibCall( com, "String", @@ -3491,7 +3507,9 @@ let log (com: ICompiler) r t (i: CallInfo) (_: Expr option) (args: Expr list) = match args with | [] -> [] | [ v ] -> [ v ] - | (StringConst _) :: _ -> [ Helper.LibCall(com, "String", "format", t, args, i.SignatureArgTypes) ] + | (StringConst _ as format) :: formatArgs -> + let args = format :: List.map normalizeIntegerFormatArg formatArgs + [ Helper.LibCall(com, "String", "format", t, args, i.SignatureArgTypes) ] | _ -> [ args.Head ] Helper.GlobalCall("console", t, args, memb = "log", ?loc = r) diff --git a/tests/Js/Main/ConvertTests.fs b/tests/Js/Main/ConvertTests.fs index 8bdfc1bdd7..933ab8df73 100644 --- a/tests/Js/Main/ConvertTests.fs +++ b/tests/Js/Main/ConvertTests.fs @@ -1499,6 +1499,19 @@ let tests = values |> List.map (fun x -> x.ToString()) |> equal expected values |> List.map (fun x -> $"{x}") |> equal expected + testCase "Negated integer zero is written without a sign" + <| fun () -> + let x = List.head [ 0 ] + String.Format("{0}", -x) |> equal "0" + String.Format("{0}", x * -1) |> equal "0" + String.Format("{0} {1} {2} {3}", -x, x * -1, -0.0, 1.5) |> equal "0 0 -0 1.5" + String.Format("{0} {1}", byte x, -(int16 x)) |> equal "0 0" + String.Format("{0:N0} {0:G}", -x) |> equal "0 0" + sprintf "%O" (-x) |> equal "0" + string (-x) |> equal "0" + (-x).ToString() |> equal "0" + $"{-x}" |> equal "0" + testCase "byte.ToString 'X' works" <| fun () -> (255uy).ToString("X2") |> equal "FF" (255uy).ToString("X15") |> equal "0000000000000FF" From 7d46c71405542c6d6e4a0204251f4b704efac847 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Tue, 3 Nov 2026 17:53:16 +0100 Subject: [PATCH 304/322] fix(js/ts): evaluate reflection parents lazily (#5243) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 13 ++++++++++- src/fable-library-ts/Reflection.ts | 15 ++++++++---- tests/Js/Main/ReflectionTests.fs | 28 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 7e00eb84ef..5582a9ae22 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -467,9 +467,20 @@ module Reflection = |> Map |> Some - yield + let parent = Fable.DeclaredType(d.Entity, d.GenericArgs) |> transformTypeInfoFor Reflection com ctx r genMap + + let rec hasDeclaredType (t: Fable.Type) = + match t with + | Fable.DeclaredType _ -> true + | t -> t.Generics |> List.exists hasDeclaredType + + // A generic argument can be this type or one that inherits from it, so evaluating it eagerly recurses forever + if d.GenericArgs |> List.exists hasDeclaredType then + yield Expression.arrowFunctionExpression ([||], parent) + else + yield parent | None -> () ] |> libReflectionCall com ctx r "class" diff --git a/src/fable-library-ts/Reflection.ts b/src/fable-library-ts/Reflection.ts index 4ee30aceba..8cb9e588bd 100644 --- a/src/fable-library-ts/Reflection.ts +++ b/src/fable-library-ts/Reflection.ts @@ -50,7 +50,8 @@ export class TypeInfo implements IEquatable { public fullname: string; public generics?: TypeInfo[]; public construct?: Constructor; - public parent?: TypeInfo; + // A thunk when the base type's generic arguments can refer back to this type, see #3607 + private lazyParent?: TypeInfo | (() => TypeInfo); public fields?: () => FieldInfo[]; public cases?: () => CaseInfo[]; public enumCases?: EnumCase[]; @@ -59,7 +60,7 @@ export class TypeInfo implements IEquatable { fullname: string, generics?: TypeInfo[], construct?: Constructor, - parent?: TypeInfo, + parent?: TypeInfo | (() => TypeInfo), fields?: () => FieldInfo[], cases?: () => CaseInfo[], enumCases?: EnumCase[] @@ -67,11 +68,17 @@ export class TypeInfo implements IEquatable { this.fullname = fullname; this.generics = generics; this.construct = construct; - this.parent = parent; + this.lazyParent = parent; this.fields = fields; this.cases = cases; this.enumCases = enumCases; } + public get parent(): TypeInfo | undefined { + if (typeof this.lazyParent === "function") { + this.lazyParent = this.lazyParent(); + } + return this.lazyParent; + } public toString() { return fullName(this); } @@ -115,7 +122,7 @@ export function class_type( fullname: string, generics?: TypeInfo[], construct?: Constructor, - parent?: TypeInfo): TypeInfo { + parent?: TypeInfo | (() => TypeInfo)): TypeInfo { return new TypeInfo(fullname, generics, construct, parent); } diff --git a/tests/Js/Main/ReflectionTests.fs b/tests/Js/Main/ReflectionTests.fs index 453e15cb43..b23c8716d9 100644 --- a/tests/Js/Main/ReflectionTests.fs +++ b/tests/Js/Main/ReflectionTests.fs @@ -53,6 +53,23 @@ type MyClass<'T1, 'T2>(v1: 'T1, v2: 'T2) = member _.Value1 = v1 member _.Value2 = v2 +[] +type SelfBase<'Inheritor when 'Inheritor :> SelfBase<'Inheritor>>() = + member _.Dummy() = 1 + +type SelfInheritor() = + inherit SelfBase() + +[] +type PairBase<'Other>() = + member _.Dummy() = 2 + +type PairPing() = + inherit PairBase() + +and PairPong() = + inherit PairBase() + let genericTests = [ testCase "typedefof works" <| fun () -> let tdef1 = typedefof @@ -705,6 +722,17 @@ let reflectionTests = [ t2.IsSubclassOf(t1) |> equal true t1.IsSubclassOf(t2) |> equal false + testCase "GetType works when a generic base takes the class itself" <| fun () -> // See #3607 + let t = SelfInheritor().GetType() + t.Name |> equal "SelfInheritor" + t = typeof |> equal true + t.IsSubclassOf(typeof>) |> equal true + SelfInheritor().Dummy() |> equal 1 + let ping = PairPing().GetType() + ping.Name |> equal "PairPing" + ping.IsSubclassOf(typeof>) |> equal true + PairPong().GetType().IsSubclassOf(typeof>) |> equal true + testCase ".GetInterface works when types are know at compile time" <| fun () -> // #2321 let t = typeof>.GetInterface("MyInterface`1") t.GetGenericArguments().[0] = typeof |> equal true From ad01c59fef82b7841745fc84938059cedb7905ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 08:41:41 +0100 Subject: [PATCH 305/322] chore: release multiple projects (#5225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🤖 easybuild-shipit --- src/Fable.Cli/CHANGELOG.md | 36 ++++++++++++++++++++++++- src/Fable.Compiler/CHANGELOG.md | 36 ++++++++++++++++++++++++- src/Fable.Transforms/Global/Compiler.fs | 4 +-- src/fable-compiler-js/CHANGELOG.md | 31 ++++++++++++++++++++- src/fable-compiler-js/package.json | 4 +-- src/fable-library-ts/CHANGELOG.md | 24 ++++++++++++++++- src/fable-library-ts/package.json | 2 +- src/fable-standalone/CHANGELOG.md | 34 ++++++++++++++++++++++- src/fable-standalone/package.json | 4 +-- 9 files changed, 163 insertions(+), 12 deletions(-) diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 99077cc6ce..541f521959 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -28,6 +28,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.27.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Recompile when a source file is added or its output is missing (#4870) ([a782afce](https://github.com/fable-compiler/Fable/commit/a782afcea79f91bc5649e7db5eea57f9d11a1529)) +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Give imported .fsx files their output extension (#5241) ([60771e07](https://github.com/fable-compiler/Fable/commit/60771e077577fdcdf5e40e82630cf88e8cfad560)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + ## 5.26.0 - 2026-10-29 ### 🚀 Features diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 077c24ac7d..1fd3d0d9ac 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../fable-library-beam/ - ../fable-library-dart/ @@ -21,6 +21,40 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.26.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Recompile when a source file is added or its output is missing (#4870) ([a782afce](https://github.com/fable-compiler/Fable/commit/a782afcea79f91bc5649e7db5eea57f9d11a1529)) +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Give imported .fsx files their output extension (#5241) ([60771e07](https://github.com/fable-compiler/Fable/commit/60771e077577fdcdf5e40e82630cf88e8cfad560)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + ## 5.25.0 - 2026-10-29 ### 🚀 Features diff --git a/src/Fable.Transforms/Global/Compiler.fs b/src/Fable.Transforms/Global/Compiler.fs index da346cc1af..91ab47a3f9 100644 --- a/src/Fable.Transforms/Global/Compiler.fs +++ b/src/Fable.Transforms/Global/Compiler.fs @@ -2,10 +2,10 @@ namespace Fable module Literals = [] - let VERSION = "5.26.0" + let VERSION = "5.27.0" [] - let JS_LIBRARY_VERSION = "2.12.0" + let JS_LIBRARY_VERSION = "2.13.0" type CompilerOptionsHelper = static member Make diff --git a/src/fable-compiler-js/CHANGELOG.md b/src/fable-compiler-js/CHANGELOG.md index 2584f7a336..b84a463df1 100644 --- a/src/fable-compiler-js/CHANGELOG.md +++ b/src/fable-compiler-js/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 include: - ../Fable.Core/ - ../fable-standalone/ @@ -16,6 +16,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.5.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + ## 4.4.0 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-compiler-js/package.json b/src/fable-compiler-js/package.json index 3847043490..139c6911d9 100644 --- a/src/fable-compiler-js/package.json +++ b/src/fable-compiler-js/package.json @@ -2,7 +2,7 @@ "type": "module", "name": "@fable-org/fable-compiler-js", "private": false, - "version": "4.4.0", + "version": "4.5.0", "main": "index.js", "bin": { "fable": "index.js" @@ -25,7 +25,7 @@ "homepage": "https://github.com/fable-compiler/Fable#readme", "dependencies": { "@fable-org/fable-metadata": "^2.2.0", - "@fable-org/fable-standalone": "^4.0.0-beta.5" + "@fable-org/fable-standalone": "^4.0.0-beta.6" }, "devDependencies": { "esbuild": "^0.28.1", diff --git a/src/fable-library-ts/CHANGELOG.md b/src/fable-library-ts/CHANGELOG.md index 7fb47cb333..712308a38a 100644 --- a/src/fable-library-ts/CHANGELOG.md +++ b/src/fable-library-ts/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 updaters: - package.json: file: package.json @@ -15,6 +15,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.13.0 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + ## 2.12.0 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-library-ts/package.json b/src/fable-library-ts/package.json index bc92c97362..50624ea243 100644 --- a/src/fable-library-ts/package.json +++ b/src/fable-library-ts/package.json @@ -3,7 +3,7 @@ "private": false, "type": "module", "name": "@fable-org/fable-library-ts", - "version": "2.12.0", + "version": "2.13.0", "description": "Core library used by F# projects compiled with fable.io", "author": "Fable Contributors", "license": "MIT", diff --git a/src/fable-standalone/CHANGELOG.md b/src/fable-standalone/CHANGELOG.md index f33587fd25..00ea269864 100644 --- a/src/fable-standalone/CHANGELOG.md +++ b/src/fable-standalone/CHANGELOG.md @@ -1,5 +1,5 @@ --- -last_commit_released: cac5ddd9a99205e0bb5272f1ffd8f0cca937824c +last_commit_released: 7d46c71405542c6d6e4a0204251f4b704efac847 pre_release: beta include: - ../Fable.Transforms/ @@ -21,6 +21,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.0.0-beta.6 - 2026-11-03 + +### 🚀 Features + +* *(js/ts/python/dart/rust/beam)* Support Convert.ToHexString, ToHexStringLower and FromHexString (#5204) ([3a3acf4e](https://github.com/fable-compiler/Fable/commit/3a3acf4ee213abb07fc4f7b98af69a6f8e68b571)) + +### 🐞 Bug Fixes + +* *(all)* Keep reads of imported mutable values in place (#5242) ([1190dfa5](https://github.com/fable-compiler/Fable/commit/1190dfa5e5f12793fc2b3b2d15a24afb8253bce3)) +* *(all)* Rename idents of a witness inlined twice (#5249) ([1e41dcc5](https://github.com/fable-compiler/Fable/commit/1e41dcc5ac5a990a81aa15d43921d824b5466740)) +* *(js/ts)* Round float32 results like .NET (#5222) ([7b176e7e](https://github.com/fable-compiler/Fable/commit/7b176e7e13bee118a21b8358e6eed65c19ac9549)) +* *(js/ts)* Unescape doubled braces in String.Format (#5231) ([24e01fd3](https://github.com/fable-compiler/Fable/commit/24e01fd30e3eec7571816b4d6cc9f6281cce0de4)) +* *(js/ts)* Treat null as empty in String.Join and Concat (#5230) ([a7dcab8d](https://github.com/fable-compiler/Fable/commit/a7dcab8d770ac56f825e15361df7b2e05f618e5e)) +* *(js/ts)* Support MidpointRounding in Math.Round (#5233) ([5a7ea990](https://github.com/fable-compiler/Fable/commit/5a7ea990b76c928564364f4844fc9a3666b612fc)) +* *(js/ts)* Print null strings as empty ([6c22c162](https://github.com/fable-compiler/Fable/commit/6c22c162674a167e43e49ad0698f426123bc35f4)) +* *(js/ts)* Match Dictionary/HashSet array keys by reference (#5239) ([1d77d628](https://github.com/fable-compiler/Fable/commit/1d77d6282b8dea284bca3cff9a6455e561575e3a)) +* *(js/ts)* Round exact midpoints to even in numeric formats (#5245) ([350a3900](https://github.com/fable-compiler/Fable/commit/350a390071473bb4e93fb055a3876d7b3215ada1)) +* *(js/ts)* Round Math.Round ties on the exact value (#5254) ([8014fb20](https://github.com/fable-compiler/Fable/commit/8014fb20fed77653aa8f119cfb080d86b59ae408)) +* *(js/ts)* Escape */ in JSDoc comments (#5255) ([1072231c](https://github.com/fable-compiler/Fable/commit/1072231c4c64467547d7f69ab741824f2e1a522c)) +* *(js/ts)* Write floats like .NET's ToString (#5246) ([c958156c](https://github.com/fable-compiler/Fable/commit/c958156c9f5eb631d78ac331e93c4cfa85e0962a)) +* *(js/ts)* Keep format of interpolated StringFormat (#5248) ([03712db9](https://github.com/fable-compiler/Fable/commit/03712db9d22e3aa685ec9181aea3ee5c90bb7303)) +* *(js/ts)* Write negated integer zero without a sign (#5256) ([ce7fc717](https://github.com/fable-compiler/Fable/commit/ce7fc717c7597b133dc6f999f88a15b2fb636d65)) +* *(js/ts)* Evaluate reflection parents lazily (#5243) ([7d46c714](https://github.com/fable-compiler/Fable/commit/7d46c71405542c6d6e4a0204251f4b704efac847)) +* *(js/ts/python)* Call base ctor from explicit constructors (#5226) ([508026e7](https://github.com/fable-compiler/Fable/commit/508026e73511f2636245515b8ec9476176e34b5c)) +* *(js/ts/python)* Clamp negative slice bounds like .NET (#5232) ([7319a994](https://github.com/fable-compiler/Fable/commit/7319a994a9c3be7d31348d29d2c464482b5f21f9)) +* *(js/ts/python/dart/rust/beam)* Use FSharp.Core's algorithms for random collection functions (#5197) ([dd860cc1](https://github.com/fable-compiler/Fable/commit/dd860cc179e0802d46eebb4110580e1edbf64cb8)) +* *(rust)* Inline hashing, more Decimal tests (#5236) ([705b6b70](https://github.com/fable-compiler/Fable/commit/705b6b70bf0b732811bd6e69cfc8fc0614b831c7)) +* *(ts)* Declare Mangle(false) interface members unmangled (#5234) ([86c00004](https://github.com/fable-compiler/Fable/commit/86c000041f286668353615003692b07e9082ff01)) +* *(ts)* Extend library classes for BCL collection bases (#5240) ([d82ef893](https://github.com/fable-compiler/Fable/commit/d82ef8932c58ddc73ff5aa6ca669be86aedbe9db)) + +[View changes on Github](https://github.com/fable-compiler/Fable/compare/cac5ddd9a99205e0bb5272f1ffd8f0cca937824c..7d46c71405542c6d6e4a0204251f4b704efac847) + ## 4.0.0-beta.5 - 2026-10-29 ### 🚀 Features diff --git a/src/fable-standalone/package.json b/src/fable-standalone/package.json index be2e5e066f..80d3800b8b 100644 --- a/src/fable-standalone/package.json +++ b/src/fable-standalone/package.json @@ -2,8 +2,8 @@ "type": "module", "name": "@fable-org/fable-standalone", "private": false, - "version": "4.0.0-beta.5", - "fableVersion": "5.26.0", + "version": "4.0.0-beta.6", + "fableVersion": "5.27.0", "main": "./dist/bundle.min.js", "description": "Fable compiler", "keywords": [ From 3c1cea99d30edc2cf3fa5dbeb3a3e6a00e97235a Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 4 Nov 2026 15:24:17 +0100 Subject: [PATCH 306/322] fix(js/ts): keep TimeSpan.Ticks exact for big values (#5250) --- src/fable-library-ts/TimeSpan.ts | 2 +- src/fable-library-ts/TimeSpanTemporal.ts | 2 +- tests/Js/Main/TimeSpanTests.fs | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/fable-library-ts/TimeSpan.ts b/src/fable-library-ts/TimeSpan.ts index 904ebc558a..7b27de262f 100644 --- a/src/fable-library-ts/TimeSpan.ts +++ b/src/fable-library-ts/TimeSpan.ts @@ -76,7 +76,7 @@ export function milliseconds(ts: TimeSpan) { } export function ticks(ts: TimeSpan) { - return toInt64(fromFloat64(ts * 10000)); + return toInt64(Number.isInteger(ts) ? BigInt(ts) * 10000n : fromFloat64(ts * 10000)); } export function totalDays(ts: TimeSpan) { diff --git a/src/fable-library-ts/TimeSpanTemporal.ts b/src/fable-library-ts/TimeSpanTemporal.ts index e0011f651a..70ad0b265a 100644 --- a/src/fable-library-ts/TimeSpanTemporal.ts +++ b/src/fable-library-ts/TimeSpanTemporal.ts @@ -117,7 +117,7 @@ export function fromSeconds(s: number | bigint, ms: bigint = 0n): TimeSpan { } export function fromMilliseconds(ms: number | bigint): TimeSpan { - return fromNanoseconds(unitToNanoseconds(Number(ms), nsPerMillisecond)); + return fromTicks(typeof ms === "bigint" || Number.isInteger(ms) ? BigInt(ms) * 10000n : BigInt(Math.trunc(ms * 10000))); } export function ticks(ts: TimeSpan): int64 { diff --git a/tests/Js/Main/TimeSpanTests.fs b/tests/Js/Main/TimeSpanTests.fs index eef348bfeb..78a7a15502 100644 --- a/tests/Js/Main/TimeSpanTests.fs +++ b/tests/Js/Main/TimeSpanTests.fs @@ -139,6 +139,24 @@ let tests = t.Ticks |> equal 20000L + testCase "TimeSpan.Ticks works with big values" <| fun () -> + TimeSpan(10675199, 2, 48, 5, 477).Ticks |> equal 9223372036854770000L + TimeSpan(-10675199, -2, -48, -5, -477).Ticks |> equal -9223372036854770000L + TimeSpan.FromSeconds(922337203685L).Ticks |> equal 9223372036850000000L + TimeSpan.FromMinutes(15372286728.).Ticks |> equal 9223372036800000000L + TimeSpan.FromDays(10675199.).Ticks |> equal 9223371936000000000L + TimeSpan.FromMilliseconds(922337203685477L).Ticks |> equal 9223372036854770000L + + testCase "TimeSpan.Ticks keeps sub-millisecond values" <| fun () -> + TimeSpan.FromMilliseconds(1.0001).Ticks |> equal 10001L + TimeSpan.FromMilliseconds(1.0005).Ticks |> equal 10005L + TimeSpan.FromMilliseconds(-1.0001).Ticks |> equal -10001L + TimeSpan.FromSeconds(0.0010001).Ticks |> equal 10001L + (TimeSpan.FromMilliseconds(1.0) * 1.0001).Ticks |> equal 10001L + TimeSpan.FromMilliseconds(900719925474.1).Ticks |> equal 9007199254741000L + TimeSpan.FromMilliseconds(-900719925474.1).Ticks |> equal -9007199254741000L + TimeSpan.FromMilliseconds(1e12 + 0.3).Ticks |> equal 10000000000003000L + // NOTE: This test fails because of very small fractions, so I cut the fractional part testCase "TimeSpan totals work" <| fun () -> let t = TimeSpan.FromMilliseconds(96441615.) From dd668b28234708ef2bc04b9056d1548f92552dcb Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 4 Nov 2026 15:59:30 +0100 Subject: [PATCH 307/322] perf(js/ts): read arrays at the call site (#5264) --- src/Fable.Transforms/Replacements.fs | 20 ++++++++++++++----- .../AnonymousLambda.jsx.expected | 4 ++-- .../inlineIfLambda/NamedFunction.jsx.expected | 4 ++-- .../NamedLambdaValue.jsx.expected | 4 ++-- tests/Js/Main/ArrayTests.fs | 17 ++++++++++++++++ 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index a59ee3112e..6c0eb6369c 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2298,6 +2298,20 @@ let binarySearch (com: ICompiler) ctx r t rangeMeth (ar: Expr) args = | [ index; length; value; comparer ] -> call rangeMeth [ index; length; value ] (Some comparer) | _ -> None +let arrayGetItem (com: ICompiler) r (t: Type) (args: Expr list) (signatureArgTypes: Type list) = + let item = + Helper.LibCall(com, "Array", "item", t, args, signatureArgTypes, ?loc = r) + + let canRepeat e = + match e with + | IdentExpr _ -> true + | e -> not (canHaveSideEffects com e) + + match args with + // V8 cannot specialise the read inside the shared `item` once it sees many array kinds, and only a nullish read can be out of bounds + | [ idx; ar ] when canRepeat idx && canRepeat ar -> emitExpr r t [ getExpr None t ar idx; item ] "$0 ?? $1" |> Some + | _ -> Some item + let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = match i.CompiledName, thisArg, args with | ".ctor", _, [] -> makeResizeArray (getElementType t) [] |> Some @@ -2309,7 +2323,7 @@ let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (this Helper.GlobalCall("Array", t, args, memb = "from", ?loc = r) |> withTag "array" |> Some - | "get_Item", Some ar, [ idx ] -> Helper.LibCall(com, "Array", "item", t, [ idx; ar ], ?loc = r) |> Some + | "get_Item", Some ar, [ idx ] -> arrayGetItem com r t [ idx; ar ] [] | "set_Item", Some ar, [ idx; value ] -> Helper.LibCall(com, "Array", "setItem", t, [ ar; idx; value ], ?loc = r) |> Some | "CopyTo", Some ar, [ target ] -> @@ -2494,10 +2508,6 @@ let arrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: E |> Some | _ -> None -let arrayGetItem (com: ICompiler) r (t: Type) (args: Expr list) (signatureArgTypes: Type list) = - Helper.LibCall(com, "Array", "item", t, args, signatureArgTypes, ?loc = r) - |> Some - let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Expr option) (args: Expr list) = let newArrayAlloc size t = Value(NewArray(ArrayAlloc size, t, MutableArray), None) diff --git a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected index c439bf61a3..8ff5422f59 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected @@ -11,11 +11,11 @@ export let sum = createAtom(0); (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x = item(i, array_1) | 0; + const x = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x = item(i_1, array_1) | 0; + const x = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x); } })(); diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected index 6cef943fe9..89cd4cfb84 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected @@ -15,11 +15,11 @@ export function addToSum(x) { (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x_1 = item(i, array_1) | 0; + const x_1 = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x_1); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x_1 = item(i_1, array_1) | 0; + const x_1 = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x_1); } })(); diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected index 08f3dd85bc..6875caa3f0 100644 --- a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected @@ -15,11 +15,11 @@ export function myAction(x) { (function () { const array_1 = arr; for (let i = 0; i <= (array_1.length - 1); i++) { - const x_1 = item(i, array_1) | 0; + const x_1 = (array_1[i] ?? item(i, array_1)) | 0; sum(sum() + x_1); } for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { - const x_1 = item(i_1, array_1) | 0; + const x_1 = (array_1[i_1] ?? item(i_1, array_1)) | 0; sum(sum() + x_1); } })(); diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 2fd87be533..029620cd3c 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -295,6 +295,23 @@ let tests = xs[2] |> equal (Some 3) throwsAnyError (fun () -> xs[3]) + testCase "Array getter evaluates the array and the index once" <| fun () -> + let mutable calls = 0 + let xs = [| 10; 20; 30 |] + let next () = + calls <- calls + 1 + calls + xs.[next ()] |> equal 20 + calls |> equal 1 + let ys = ResizeArray [ "a"; "b" ] + let pick () = + calls <- calls + 1 + ys + (pick ()).[1] |> equal "b" + calls |> equal 2 + throwsAnyError (fun () -> xs.[next ()]) + calls |> equal 3 + testCase "Array setter works" <| fun () -> let x = [| 1.; 2.; 3.; 4.; 5. |] Array.set x 3 10. From 07a38e764b1dd8fb79b4374df2aa22c18e0b7fce Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 5 Nov 2026 10:40:07 +0100 Subject: [PATCH 308/322] perf(js/ts): read string chars at the call site --- src/Fable.Transforms/Replacements.fs | 28 ++++++++++++++++++++-------- tests/Js/Main/StringTests.fs | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 6c0eb6369c..6f5a88f514 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -1518,6 +1518,21 @@ let (|PrintfNoFormatSpecifiers|_|) (arg: Expr) = | _ -> None | _ -> None +// The fallback only runs for a nullish read, and every out-of-bounds read is nullish +let readAtCallSite (com: ICompiler) r t container idx (fallback: Expr) = + let rec canRepeat e = + match e with + | IdentExpr _ + | Value((StringConstant _ | NumberConstant _ | CharConstant _ | BoolConstant _), _) -> true + | TypeCast(e, _) -> canRepeat e + | Get(e, FieldGet info, _, _) -> not info.CanHaveSideEffects && canRepeat e + | _ -> false + + if canRepeat container && canRepeat idx then + emitExpr r t [ getExpr None t container idx; fallback ] "$0 ?? $1" + else + fallback + let (|InterpolatedPrintf|_|) (arg: Expr) = match arg with | MaybeCasted(Call(Import({ Selector = "interpolatedPrintf" }, _, _), callInfo, _, _)) -> @@ -1953,8 +1968,9 @@ let strings (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr opt |> Some | _ -> fsFormat com ctx r t i thisArg args | "get_Length", Some c, _ -> getFieldWith r t c "length" |> Some - | "get_Chars", Some c, _ -> + | "get_Chars", Some c, [ idx ] -> Helper.LibCall(com, "String", "getCharAtIndex", t, args, i.SignatureArgTypes, thisArg = c, ?loc = r) + |> readAtCallSite com r t c idx |> Some | "Equals", Some x, [ y ] | "Equals", None, [ x; y ] -> makeEqOp r x y BinaryEqual |> Some @@ -2302,14 +2318,9 @@ let arrayGetItem (com: ICompiler) r (t: Type) (args: Expr list) (signatureArgTyp let item = Helper.LibCall(com, "Array", "item", t, args, signatureArgTypes, ?loc = r) - let canRepeat e = - match e with - | IdentExpr _ -> true - | e -> not (canHaveSideEffects com e) - match args with - // V8 cannot specialise the read inside the shared `item` once it sees many array kinds, and only a nullish read can be out of bounds - | [ idx; ar ] when canRepeat idx && canRepeat ar -> emitExpr r t [ getExpr None t ar idx; item ] "$0 ?? $1" |> Some + // V8 cannot specialise the read inside the shared `item` once it sees many array kinds + | [ idx; ar ] -> readAtCallSite com r t ar idx item |> Some | _ -> Some item let resizeArrays (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (thisArg: Expr option) (args: Expr list) = @@ -3197,6 +3208,7 @@ let intrinsicFunctions (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisAr | "MakeDecimal", _, _ -> decimals com ctx r t i thisArg args | "GetString", _, [ str; idx ] -> Helper.LibCall(com, "String", "getCharAtIndex", t, [ str; idx ], i.SignatureArgTypes, ?loc = r) + |> readAtCallSite com r t str idx |> Some | "GetArray", _, [ ar; idx ] -> arrayGetItem com r t [ idx; ar ] (i.SignatureArgTypes |> List.rev) | "SetArray", _, [ ar; idx; value ] -> setExpr r ar idx value |> Some diff --git a/tests/Js/Main/StringTests.fs b/tests/Js/Main/StringTests.fs index b1a3f38038..57b1ba0494 100644 --- a/tests/Js/Main/StringTests.fs +++ b/tests/Js/Main/StringTests.fs @@ -1258,6 +1258,21 @@ let tests = testList "Strings" [ throwsAnyError <| fun () -> input.Chars(3) |> ignore + testCase "String indexer evaluates the string and the index once" <| fun () -> + let mutable calls = 0 + let next () = + calls <- calls + 1 + calls + let pick () = + calls <- calls + 1 + "abc" + "abc".[next ()] |> equal 'b' + calls |> equal 1 + (pick ()).[2] |> equal 'c' + calls |> equal 2 + throwsAnyError (fun () -> "abc".[next () + 2]) + calls |> equal 3 + testCase "String indexer works" <| fun () -> let input = "hello" input.[1] |> equal 'e' From 66ace46864b628dd4a5b8b71f121c282892f2e0e Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 5 Nov 2026 11:10:32 +0100 Subject: [PATCH 309/322] fix(js/ts): evaluate the Array.create count once --- src/Fable.Transforms/Replacements.fs | 10 +++++++++- tests/Js/Main/ArrayTests.fs | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Fable.Transforms/Replacements.fs b/src/Fable.Transforms/Replacements.fs index 6f5a88f514..074a619f71 100644 --- a/src/Fable.Transforms/Replacements.fs +++ b/src/Fable.Transforms/Replacements.fs @@ -2529,7 +2529,15 @@ let arrayModule (com: ICompiler) (ctx: Context) r (t: Type) (i: CallInfo) (_: Ex | Array(t2, _), value -> let value = value |> Option.defaultWith (fun () -> getZero com ctx t2) // If we don't fill the array some operations may behave unexpectedly, like Array.prototype.reduce - Helper.LibCall(com, "Array", "fill", t, [ newArrayAlloc size t2; makeIntConst 0; size; value ]) + let fill size = + Helper.LibCall(com, "Array", "fill", t, [ newArrayAlloc size t2; makeIntConst 0; size; value ]) + + match size with + | IdentExpr _ + | Value(NumberConstant _, _) -> fill size + | _ -> + let n = makeUniqueIdent com ctx size.Type "n" + Let(n, size, fill (IdentExpr n)) | _ -> $"Expecting an array type but got %A{t}" |> addErrorAndReturnNull com ctx.InlinePath r diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 029620cd3c..86e7a5cb28 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -167,6 +167,16 @@ let tests = xs |> equal [| 1uy; 0uy |] #endif + testCase "Array.zeroCreate and Array.create evaluate the count once" <| fun () -> + let mutable calls = 0 + let size () = + calls <- calls + 1 + 2 + Array.zeroCreate (size ()) |> equal [| 0M; 0M |] + calls |> equal 1 + Array.create (size ()) "a" |> equal [| "a"; "a" |] + calls |> equal 2 + testCase "Mapping from values to functions works" <| fun () -> let a = [| "a"; "b"; "c" |] let b = [| 1; 2; 3 |] From 3925e5c169b50c7743528d301919f54b861ec4ca Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Thu, 5 Nov 2026 11:10:34 +0100 Subject: [PATCH 310/322] test(js/ts): check the array fast path evaluates once --- tests/Js/Main/ArrayTests.fs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Js/Main/ArrayTests.fs b/tests/Js/Main/ArrayTests.fs index 86e7a5cb28..0d70eb736f 100644 --- a/tests/Js/Main/ArrayTests.fs +++ b/tests/Js/Main/ArrayTests.fs @@ -296,6 +296,16 @@ let tests = let b = [| |] throwsAnyError (fun () -> b.[0]) + testCase "Array indexer evaluates the array once" <| fun () -> + let mutable calls = 0 + let size () = + calls <- calls + 1 + 3 + (Array.zeroCreate (size ())).[1] |> equal 0 + calls |> equal 1 + throwsAnyError (fun () -> (Array.zeroCreate (size ())).[5]) + calls |> equal 2 + testCase "Array getter returns None for an in-bounds option element" <| fun () -> let xs = [| Some 1; None; Some 3 |] Array.get xs 1 |> equal None From 34763540c2068dd11e1e43763bd0e3a3829761b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:38:22 +0100 Subject: [PATCH 311/322] chore: Bump @fluentui/react from 8.125.8 to 8.125.10 in /tests/React in the all-npm group (#5258) Signed-off-by: dependabot[bot] --- tests/React/package-lock.json | 102 +++++++++++++++++----------------- tests/React/package.json | 2 +- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/tests/React/package-lock.json b/tests/React/package-lock.json index 18b9651e25..f1d1c1f123 100644 --- a/tests/React/package-lock.json +++ b/tests/React/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.8", + "@fluentui/react": "^8.125.10", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.8", @@ -721,27 +721,27 @@ } }, "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.75", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.75.tgz", - "integrity": "sha512-BSXGRmANjOpYvDboBf6j50vJCLJvb3d4DoOZS6xkyDMwEXdWO1ExTM+VECtA67FpmdSRRlZFpayTCqQ9hJkL2Q==", + "version": "8.5.76", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.76.tgz", + "integrity": "sha512-lC5WfGQq2qUu8tGUyu0TK84N+K4qxjdF6jJgocJni4UhFMr7xBCljl/AhSFY+hUlbnMF8qw0SY/FhFYlF/f4fA==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.3", - "@fluentui/utilities": "^8.17.3", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" } }, "node_modules/@fluentui/foundation-legacy": { - "version": "8.6.8", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.8.tgz", - "integrity": "sha512-Z00gGL7l+l/qwovK7E52cjOAccCpOt8Ziw4JUYhbo+qsAn59auE6xrH+6HwwHibKqKymSO2KsJ4vkpJC4iT0MA==", + "version": "8.6.9", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.6.9.tgz", + "integrity": "sha512-1haga7Idbz59IbB1Pq+FNXVm0CcH/jZiuVQ8vYWjcOfUdDApHrBYk20gUmBjOVzyviQVqm9Ts4sh8k36FwtUxQ==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.15", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.3", - "@fluentui/utilities": "^8.17.3", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -759,9 +759,9 @@ } }, "node_modules/@fluentui/merge-styles": { - "version": "8.6.15", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.15.tgz", - "integrity": "sha512-aNob5pyHJ9wxyr/E79+YTrS9B0kkUcaHoY0dfeCVM+VF4IvWOr8u/9Cxan7vpYUMXy93QWPeDxAVmlzsRTlEpA==", + "version": "8.6.16", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.6.16.tgz", + "integrity": "sha512-HeqTdVPpSElxzONf6jRbL6CvYACey/Lj/aI5yNVzGrz/68krZKiUaBI1IE+Q+AnliSNUoPKeqti4Lyyify0Eow==", "license": "MIT", "dependencies": { "@fluentui/set-version": "^8.2.24", @@ -769,23 +769,23 @@ } }, "node_modules/@fluentui/react": { - "version": "8.125.8", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.8.tgz", - "integrity": "sha512-f/3RvfYCBBqACuPAVb/k3yxIPOHfdfm1AGxdoegRZXZ5cRRW/6WtGZwjb15jc6E/dMEOJjc/8BfLfjYl2A4V7A==", + "version": "8.125.10", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.125.10.tgz", + "integrity": "sha512-V40Au1Pj5g8mnsCnBDd7yTDGmGlIEeJsYixkY0NA6gV6P5IIctzSSWjAJWHWEnwidcdXUiANNOJzvlWZA03Pew==", "license": "MIT", "dependencies": { "@fluentui/date-time-utilities": "^8.6.11", - "@fluentui/font-icons-mdl2": "^8.5.75", - "@fluentui/foundation-legacy": "^8.6.8", - "@fluentui/merge-styles": "^8.6.15", - "@fluentui/react-focus": "^8.10.8", - "@fluentui/react-hooks": "^8.10.3", + "@fluentui/font-icons-mdl2": "^8.5.76", + "@fluentui/foundation-legacy": "^8.6.9", + "@fluentui/merge-styles": "^8.6.16", + "@fluentui/react-focus": "^8.10.9", + "@fluentui/react-hooks": "^8.10.4", "@fluentui/react-portal-compat-context": "^9.0.17", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.3", - "@fluentui/theme": "^2.7.3", - "@fluentui/utilities": "^8.17.3", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/theme": "^2.7.4", + "@fluentui/utilities": "^8.17.4", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" }, @@ -797,16 +797,16 @@ } }, "node_modules/@fluentui/react-focus": { - "version": "8.10.8", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.8.tgz", - "integrity": "sha512-k3845czVJHJsURlLypIEbl6A3gh9PAc+Y/41FvBQEcgaTw2BNS7rNQgq4xks3iKy6JcmpY/7ZZI/4BT/CBB0rg==", + "version": "8.10.9", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.10.9.tgz", + "integrity": "sha512-yi/GhJCogcY8lLLkWoSmzH1nVdmHxlokuRw3GOr3kcv1IX4mmFZ0fFwcVVp5ik0BTc6SeDD4xBUXdNQIXgSyKA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-key": "^0.4.23", - "@fluentui/merge-styles": "^8.6.15", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/style-utilities": "^8.15.3", - "@fluentui/utilities": "^8.17.3", + "@fluentui/style-utilities": "^8.15.4", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -815,14 +815,14 @@ } }, "node_modules/@fluentui/react-hooks": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.3.tgz", - "integrity": "sha512-fFy8Rhs/dpFCJ3X+2jv9pc4KRn6iEOaRqBqaUXKjOvIWyQDe8/LGupRWUYEHdniroZcPWEbgGulFYjLvGP0wzw==", + "version": "8.10.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.10.4.tgz", + "integrity": "sha512-nwJfVHccX7wcPaY2JGLyzzZBlg5DSAPxNpET0t1d2dduQsRJMo/fbSpblpgTwF0z3bP0kPAxAXgb1vz34Sud2Q==", "license": "MIT", "dependencies": { "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.3", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -872,28 +872,28 @@ } }, "node_modules/@fluentui/style-utilities": { - "version": "8.15.3", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.3.tgz", - "integrity": "sha512-zuyR421wNDN9CTykQABWNHm1X/3k+uTRDYQGvrpxLw8PKI9rB8OhmE+oZZLq2rzh1R3ZUiQV3+6LTe9D/8FYFA==", + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.15.4.tgz", + "integrity": "sha512-x9gieJHscmrBByc2qQgmkNfz19Ghyp7RHHTiqJEDYVHnLoRsVneuhvDqJGDCgmd3JJOxeoTPoqwF5sj1Q0ZghA==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.15", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/theme": "^2.7.3", - "@fluentui/utilities": "^8.17.3", + "@fluentui/theme": "^2.7.4", + "@fluentui/utilities": "^8.17.4", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "node_modules/@fluentui/theme": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.3.tgz", - "integrity": "sha512-DrzVYJS8gIxMsbZ8CI/hFQu4LcDsPyVag5McKCytrrvRLBAskD4bRdkTlyljhK2jUEDCsVMl9KWxnqsqy7SVHw==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.4.tgz", + "integrity": "sha512-EDEyMyxG6ygMN5XRHaM7ImVwdfQyxJe6jvig018N7HUv580mRsIxP7uV5P80Y4no1cJF6kJVp2PlLFQug0rEUg==", "license": "MIT", "dependencies": { - "@fluentui/merge-styles": "^8.6.15", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.3", + "@fluentui/utilities": "^8.17.4", "tslib": "^2.1.0" }, "peerDependencies": { @@ -902,13 +902,13 @@ } }, "node_modules/@fluentui/utilities": { - "version": "8.17.3", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.3.tgz", - "integrity": "sha512-lMk+/k7kVGXSlLC86Z0MlMrL5aeaLuDbF2GG25hiWqMBqh2dJVeqwgqruZDt5rYXs0SEBAls3YvvN/IR81PQyw==", + "version": "8.17.4", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.4.tgz", + "integrity": "sha512-qoteV6ZrV0MPv/g/Qb+KE+OeIelJOxvI2dFyoV9RXut3WjP+PAaQqheaWPU+zeQvnzgTfs52I+PbiI7qZgdLBg==", "license": "MIT", "dependencies": { "@fluentui/dom-utilities": "^2.3.10", - "@fluentui/merge-styles": "^8.6.15", + "@fluentui/merge-styles": "^8.6.16", "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", "tslib": "^2.1.0" diff --git a/tests/React/package.json b/tests/React/package.json index 49c6047c0f..e6c36afa4c 100644 --- a/tests/React/package.json +++ b/tests/React/package.json @@ -9,7 +9,7 @@ "dependencies": { "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-react-jsx": "^7.29.7", - "@fluentui/react": "^8.125.8", + "@fluentui/react": "^8.125.10", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.8", From 400f44304935f4ae40de1bbb551d1d13fb992de4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:38:33 +0100 Subject: [PATCH 312/322] chore: Bump the all-npm group with 2 updates (#5259) Signed-off-by: dependabot[bot] --- package-lock.json | 238 ++++++++++++++++++++++++---------------------- package.json | 4 +- 2 files changed, 128 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07780f8783..b840752c01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,11 @@ "name": "fable-compiler", "version": "0.0.0", "dependencies": { - "@types/node": "^26.6.5", + "@types/node": "^26.6.6", "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.65.0", + "rollup": "^4.67.3", "terser": "^5.51.2", "typescript": "^7.0.2" }, @@ -88,9 +88,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", - "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.67.3.tgz", + "integrity": "sha512-o4kGHKu2YRn/jegh1XMs6+1/3SuX6AfWaACGEWavFvN5D6YZd9OrMEBqcIMj0uTeKZr6nTXPhlpkc0IXC6DMXQ==", "cpu": [ "arm" ], @@ -101,9 +101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", - "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.67.3.tgz", + "integrity": "sha512-vfEEFajQ0K5crrtAAyownI2ZFthoY+BTwGnjoMgd4+l3YPuAcweykaMqcMYXT1wJKCnMiSIpNnAroAWix2fJ3w==", "cpu": [ "arm64" ], @@ -113,10 +113,23 @@ "android" ] }, + "node_modules/@rollup/rollup-android-x64": { + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-x64/-/rollup-android-x64-4.67.3.tgz", + "integrity": "sha512-/oNPEUU5nVDZmp1CYGDMIGdUBzON5z5RQufJbtjnOD4+CZYt//2ECHRvvmJL3EXLHaqQ7Zjr1dSTlhLCxJJbGQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", - "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.67.3.tgz", + "integrity": "sha512-Otbw33hEDwrM/jUn3nib5JLJL0vyTE0ZlmU8L44sCS+AJm0JiZs3ZlNUYj141L4k7XxYmOWtf+9cN2Kqog7OzA==", "cpu": [ "arm64" ], @@ -127,9 +140,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", - "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.67.3.tgz", + "integrity": "sha512-3CNtrc+6X9HL+fzSkKF0o3WkBdO6YBm5PhbLnR9PeQNKLLL7ir4MDtlx6gvP9ZM3Uxjue3xVysAX7+VjWX66Ng==", "cpu": [ "x64" ], @@ -140,9 +153,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", - "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.67.3.tgz", + "integrity": "sha512-KfHwLGjSfgOKHoyzhqqKNhohxNErfYRR2dNWXN6kn5iWMrDNldZopob+Ft/q6aurweYHRz+7M9HuFZ7nvYj5Xg==", "cpu": [ "arm64" ], @@ -153,9 +166,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", - "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.67.3.tgz", + "integrity": "sha512-fXWMWNshsQrNi9pq8FJIpTZfLjhLUqDCabW1hq2Qv1kxzz7I63fRjFRNAC0C49xK9CyUngL/tWbSYjpKo2+GRg==", "cpu": [ "x64" ], @@ -166,9 +179,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", - "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.67.3.tgz", + "integrity": "sha512-22gepfGL1b0Aay36BYddC4mqvQoUgQwGU/5zrUK2BdJucWgWytbhxY4o3iSIbtZsSsFiiRlNT2xfAjrCsDQGQw==", "cpu": [ "arm" ], @@ -182,9 +195,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", - "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.67.3.tgz", + "integrity": "sha512-l04417gOo3y7xaQu52PeXGgy17dSPAa8qhs4iqxMDHKyAUpldNFsasy0kR+Z7CFifnTgwFGmyukDWLIZYEnyQQ==", "cpu": [ "arm" ], @@ -198,9 +211,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", - "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.67.3.tgz", + "integrity": "sha512-0SEhbnWkUv29OhSANlnnOKQSWs5KFoJ0FTQ3ZhxZunUjgBagQzzUmLgxpulYSpldXAGOCK5XLvvspoPngjEWpQ==", "cpu": [ "arm64" ], @@ -214,9 +227,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", - "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.67.3.tgz", + "integrity": "sha512-2wLIbgSqKUcEc4DHrQDHOg9LSfgfSr+rAoBj9wromzLMLSclupvEKHTEugGSuGuklrF7wF0dzXo9R5VymKXgcg==", "cpu": [ "arm64" ], @@ -230,9 +243,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", - "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.67.3.tgz", + "integrity": "sha512-cV3cQixj3ILfdOmvJeXu6GedaH/Xlm4/qtNAUxvVK0tQnzICits97mwuvWY/DZs0lEZTkbCqZmpO/+ne6hs5iQ==", "cpu": [ "loong64" ], @@ -246,9 +259,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", - "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.67.3.tgz", + "integrity": "sha512-DTnriHdEmvrGgPWHVoV8LfwkLsmG2oK9GYvlfV6+beMcdVWVYnNC3R6+jL9DhTVOIpv+Ys9A+1i9rM8aeayOZw==", "cpu": [ "loong64" ], @@ -262,9 +275,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", - "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.67.3.tgz", + "integrity": "sha512-2+v1frx+8t084SIinkBBURODj1lpTqLZO53INfozQa0+bnCNEGtRFag8Gi6OMV9kEK67zIRIqncfyKe1eIYGHA==", "cpu": [ "ppc64" ], @@ -278,9 +291,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", - "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.67.3.tgz", + "integrity": "sha512-vEi902pAjMHXPXOpU4Jr58jMMiZxTf8kkhis1xiLBtLtlm6oRaX+mgFjOsqMdbGt80lS+bfnTCEsAofbK9isLA==", "cpu": [ "ppc64" ], @@ -294,9 +307,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", - "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.67.3.tgz", + "integrity": "sha512-1Nm/kYmkKDcc9gJ9cRVsRPBrNUqLEaY5b2zDk2OsQMcWd2nJiFUpQf3q4T1IEVKgyUQSMuiVNDemLXcuKh/f9Q==", "cpu": [ "riscv64" ], @@ -310,9 +323,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", - "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.67.3.tgz", + "integrity": "sha512-bx93yN/wit1aY9lja1lRh2jfRPFDBfeI9kHoytXDi1Jw6dWz2P2zd6B3oKnQOHa5fTtpHmX9/swWOA2+jWLjKw==", "cpu": [ "riscv64" ], @@ -326,9 +339,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", - "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.67.3.tgz", + "integrity": "sha512-Nt1/uFKauyn2bwGcYaIcF9DA3TORW45XrtdhThm5QPu0TNDpDC5onMNSh8fzvB+klrnLKTCISCw8lR9xpxrEFA==", "cpu": [ "s390x" ], @@ -342,9 +355,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", - "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.67.3.tgz", + "integrity": "sha512-K8HqIGH9CD57XmZFo/0T3pafDAzAoB9Iof02FEFvOXg7lBn7vlkLEjHgtZkYp+srWANrlxTSf8BolQM/27kiCw==", "cpu": [ "x64" ], @@ -358,9 +371,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", - "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.67.3.tgz", + "integrity": "sha512-6vTihCOamPjOpgcjkXOFB+Lh+LM4Eb95EPYNHjcovwxsnQrFRIJ7M664fLnGklKFOZutrhR9/o+NfgQLE4Zmdg==", "cpu": [ "x64" ], @@ -374,9 +387,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", - "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.67.3.tgz", + "integrity": "sha512-k5XxCKgPGASSGyXOhXVQQJM6VIRHteqwHBsbwhW55EoimmI2jfkPebwgg/lwcjezpvgyLu8YTTCRsdSrvHKA7A==", "cpu": [ "x64" ], @@ -387,9 +400,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", - "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.67.3.tgz", + "integrity": "sha512-WYqJUhonki85lB0iQ/Ro9BNJiGd7rG7GBZBiB/h4EUBWiUKyI3vq6EZ1aTyy5Y8qVuSIx4ZdqDyAXYiR59Vtvw==", "cpu": [ "arm64" ], @@ -400,9 +413,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", - "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.67.3.tgz", + "integrity": "sha512-WJgIOxBAnyZy2nXZdSk1xtDP0nmU41qfB47jCZ5VCxCipt2/aoImPtXMyPYB1fzXcpmNsAtYxBCY2d2WFYo00g==", "cpu": [ "arm64" ], @@ -413,9 +426,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", - "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.67.3.tgz", + "integrity": "sha512-C0vJ/s69Fll/puogTouGiFkn7kFI7nIAQc8/PTVawth9VNpNYIpv9xrZr6syRaFzSxVhTUt5zoXUFLELGs6vVA==", "cpu": [ "ia32" ], @@ -426,9 +439,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", - "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.67.3.tgz", + "integrity": "sha512-UFCIgrIBg2jU6Ajkrx+AwGz0EWpYCRTjr6wh+jBCY+A6r8pCDeeejTtMbbBszRdnmtUCzlXwwA4h/f4f3zPRSA==", "cpu": [ "x64" ], @@ -439,9 +452,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", - "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.67.3.tgz", + "integrity": "sha512-tlynfm3LIQtOCjFdT7kjyMnGrl0PbhyldgCfj7b1VxafD6uYiJFjd2ATzmYb+YnzNIzNezpK7XzFzDtPu37IeA==", "cpu": [ "x64" ], @@ -462,9 +475,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "license": "MIT" }, "node_modules/@types/katex": { @@ -482,9 +495,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", "license": "MIT", "dependencies": { "undici-types": "~8.9.0" @@ -2373,12 +2386,12 @@ } }, "node_modules/rollup": { - "version": "4.65.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", - "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", + "version": "4.67.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.67.3.tgz", + "integrity": "sha512-HJXOntSbeXzBfjUQ+YfgcOR+NV+8queObyCqWWOy2YL6bXDFE7vNRgVXlOUyrQqfJnxdXcL27PXlkC0O3ze6jQ==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@types/estree": "^1.0.10" }, "bin": { "rollup": "dist/bin/rollup" @@ -2389,31 +2402,32 @@ }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.65.0", - "@rollup/rollup-android-arm64": "4.65.0", - "@rollup/rollup-darwin-arm64": "4.65.0", - "@rollup/rollup-darwin-x64": "4.65.0", - "@rollup/rollup-freebsd-arm64": "4.65.0", - "@rollup/rollup-freebsd-x64": "4.65.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", - "@rollup/rollup-linux-arm-musleabihf": "4.65.0", - "@rollup/rollup-linux-arm64-gnu": "4.65.0", - "@rollup/rollup-linux-arm64-musl": "4.65.0", - "@rollup/rollup-linux-loong64-gnu": "4.65.0", - "@rollup/rollup-linux-loong64-musl": "4.65.0", - "@rollup/rollup-linux-ppc64-gnu": "4.65.0", - "@rollup/rollup-linux-ppc64-musl": "4.65.0", - "@rollup/rollup-linux-riscv64-gnu": "4.65.0", - "@rollup/rollup-linux-riscv64-musl": "4.65.0", - "@rollup/rollup-linux-s390x-gnu": "4.65.0", - "@rollup/rollup-linux-x64-gnu": "4.65.0", - "@rollup/rollup-linux-x64-musl": "4.65.0", - "@rollup/rollup-openbsd-x64": "4.65.0", - "@rollup/rollup-openharmony-arm64": "4.65.0", - "@rollup/rollup-win32-arm64-msvc": "4.65.0", - "@rollup/rollup-win32-ia32-msvc": "4.65.0", - "@rollup/rollup-win32-x64-gnu": "4.65.0", - "@rollup/rollup-win32-x64-msvc": "4.65.0", + "@rollup/rollup-android-arm-eabi": "4.67.3", + "@rollup/rollup-android-arm64": "4.67.3", + "@rollup/rollup-android-x64": "4.67.3", + "@rollup/rollup-darwin-arm64": "4.67.3", + "@rollup/rollup-darwin-x64": "4.67.3", + "@rollup/rollup-freebsd-arm64": "4.67.3", + "@rollup/rollup-freebsd-x64": "4.67.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.67.3", + "@rollup/rollup-linux-arm-musleabihf": "4.67.3", + "@rollup/rollup-linux-arm64-gnu": "4.67.3", + "@rollup/rollup-linux-arm64-musl": "4.67.3", + "@rollup/rollup-linux-loong64-gnu": "4.67.3", + "@rollup/rollup-linux-loong64-musl": "4.67.3", + "@rollup/rollup-linux-ppc64-gnu": "4.67.3", + "@rollup/rollup-linux-ppc64-musl": "4.67.3", + "@rollup/rollup-linux-riscv64-gnu": "4.67.3", + "@rollup/rollup-linux-riscv64-musl": "4.67.3", + "@rollup/rollup-linux-s390x-gnu": "4.67.3", + "@rollup/rollup-linux-x64-gnu": "4.67.3", + "@rollup/rollup-linux-x64-musl": "4.67.3", + "@rollup/rollup-openbsd-x64": "4.67.3", + "@rollup/rollup-openharmony-arm64": "4.67.3", + "@rollup/rollup-win32-arm64-msvc": "4.67.3", + "@rollup/rollup-win32-ia32-msvc": "4.67.3", + "@rollup/rollup-win32-x64-gnu": "4.67.3", + "@rollup/rollup-win32-x64-msvc": "4.67.3", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index bc097d971c..c7c9d1d2af 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "posttests-compiler-js": "node --test-reporter spec --test-timeout 20000 --test temp/tests-compiler-js/Main.js" }, "dependencies": { - "@types/node": "^26.6.5", + "@types/node": "^26.6.6", "concurrently": "^10.0.6", "ghreleases": "^3.0.2", "nodemon": "^3.1.14", - "rollup": "^4.65.0", + "rollup": "^4.67.3", "terser": "^5.51.2", "typescript": "^7.0.2" }, From 38973e4a1d3d373b4115e1ef952320759f749c41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:39:23 +0100 Subject: [PATCH 313/322] chore: Bump the all-nuget group with 2 updates (#5260) Signed-off-by: dependabot[bot] --- .config/dotnet-tools.json | 2 +- src/fable-standalone/src/Worker/Worker.fsproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d6c3b40842..3131c8ed0d 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fantomas": { - "version": "8.1.0", + "version": "8.1.1", "commands": [ "fantomas" ], diff --git a/src/fable-standalone/src/Worker/Worker.fsproj b/src/fable-standalone/src/Worker/Worker.fsproj index 00eb08cd09..be425edf44 100644 --- a/src/fable-standalone/src/Worker/Worker.fsproj +++ b/src/fable-standalone/src/Worker/Worker.fsproj @@ -11,7 +11,7 @@ - + From a2e55512653c51f837c44c71e558da965e49f5de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:39:26 +0100 Subject: [PATCH 314/322] chore: Bump the all-actions group with 2 updates (#5262) Signed-off-by: dependabot[bot] --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/repo-assist.lock.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index e80a8348c3..02b5901c07 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,6 +21,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - name: Install gh-aw extension - uses: github/gh-aw-actions/setup-cli@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup-cli@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: version: v0.71.5 diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml index f82d9afac5..35ffbd8ea0 100644 --- a/.github/workflows/repo-assist.lock.yml +++ b/.github/workflows/repo-assist.lock.yml @@ -132,7 +132,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -457,7 +457,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1360,7 +1360,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1560,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1743,7 +1743,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1796,7 +1796,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1897,7 +1897,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@de9d416cac8ccea1a2d582cb07c4b7db96946792 # v0.93.5 + uses: github/gh-aw-actions/setup@221ba4f4319de6b8e809d1a15528b3ae6a818aa5 # v1.0.1 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} From af0db791b4b8190cceb1d7c874c31c08c5496023 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:39:28 +0100 Subject: [PATCH 315/322] chore: Bump the all-pip group in /src/fable-library-py with 4 updates (#5261) Signed-off-by: dependabot[bot] --- src/fable-library-py/uv.lock | 330 ++++++++++++++++++----------------- 1 file changed, 167 insertions(+), 163 deletions(-) diff --git a/src/fable-library-py/uv.lock b/src/fable-library-py/uv.lock index aa0fe555fa..2a3891657b 100644 --- a/src/fable-library-py/uv.lock +++ b/src/fable-library-py/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <4.0" [[package]] @@ -166,26 +166,26 @@ wheels = [ [[package]] name = "maturin" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/71/eb078174335ff30bde1be2ad54fb6256db4b160eb3efa81163fa30f8c814/maturin-1.16.0.tar.gz", hash = "sha256:13e69ca12542999d8c5467131de72a49bb76e4c284b9a45c5783df7c0fcb3a8a", size = 396641, upload-time = "2026-10-29T14:09:43.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, - { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, - { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, - { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, - { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, - { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, - { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, - { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f8/235d4a8b3d61af3c7744582ae9e6ccb8f096e9f68f06fe62d7d03e5d7b2d/maturin-1.16.0-py3-none-linux_armv6l.whl", hash = "sha256:deacbb204a20326862e194c7df4958e0514a0411c9a3ccf228a00067c11b5c7e", size = 10425070, upload-time = "2026-10-29T14:09:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/f49a90d8c489dc5f134654ec91d12a78a51599fdc8c2b5a1bc2391bca0ac/maturin-1.16.0-py3-none-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl", hash = "sha256:131fcc17ddb3ccc48aa183d2172ebf994c322b858e1b9694fb8744d783df66f6", size = 19839189, upload-time = "2026-10-29T14:09:09.441Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8e/d65633d7c25484cb82735a51e15bc969f9874a13557e2b352d27df574c20/maturin-1.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2216d731c9324869d3bff72c0c73009bb6e640b3fa7d340174ae70dc874d1d9f", size = 10271659, upload-time = "2026-10-29T14:09:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dc/e77f490557cb5e045a75c3d18a7965425ac576235c970dfff4d903eb2c74/maturin-1.16.0-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:837db2f4d93d4899c3ac393fb3cc9eebf35b6002336e5d574f4a4fb6c49fe283", size = 10763361, upload-time = "2026-10-29T14:09:16.11Z" }, + { url = "https://files.pythonhosted.org/packages/fa/eb/535564a4211105fd64611fa4f9437c8925eded26a97d58de84565f5aebfa/maturin-1.16.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:399fadcd4af39532a173e010240c34930eacc1bb1746bb179863481c68bcf1ab", size = 10442856, upload-time = "2026-10-29T14:09:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/570e13ce81f103be6609c067a06c5bac08da5af0cf7ea700c9ae43419be9/maturin-1.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e285ec9eb10243d45d10417178cb2eb76e0cbb184d0aa4378b503489fa12ee78", size = 10298796, upload-time = "2026-10-29T14:09:21.385Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/26b40c31c354359fe4e49398113d018694c7035968fe1b569bb13c3905af/maturin-1.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:609228d639c5b888dc41364cbd318547a59f027481f53cba85838f46da4b3f02", size = 10268023, upload-time = "2026-10-29T14:09:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/faca1784d917c2eecd498648dff6a0179b299c8142cc448ea5b1183545ec/maturin-1.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7001fd3ac591820d01f2fa041552c367ec587c98649d9f10a817cc7f28e59062", size = 13739038, upload-time = "2026-10-29T14:09:26.662Z" }, + { url = "https://files.pythonhosted.org/packages/36/c1/d67e77aa07100e3b2d4c0d9ef6922646d9c4bab25cc04f91023aefc72418/maturin-1.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b0573a146803f24566f5b301a4572a8e49bcab4917175407c730da13b46e134", size = 11100193, upload-time = "2026-10-29T14:09:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/ccef52f02ab0e1f7258bc8fe0140b4991c6287458ef883af1027f946a253/maturin-1.16.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c16d3589cbddda9420237fdb1394b1a9b36483c0bce83b23de6118c908a8e940", size = 10663849, upload-time = "2026-10-29T14:09:32.183Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/3104f62dbb428f9e26e35cba2c80cfc42ba9bb1dc4968882c19d1bfe594b/maturin-1.16.0-py3-none-win32.whl", hash = "sha256:9e221c047931caa79283cc0c2278904ae1b25880dde3574b0447657d72907689", size = 9124894, upload-time = "2026-10-29T14:09:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/c25ce706c4797ee1663bcf1f386ffe91d8a3b3589700a135a204b49bc4b0/maturin-1.16.0-py3-none-win_amd64.whl", hash = "sha256:9d43a2adf51a2794e907a8f7dbad06eb114d5edb0909c73c1fa9040f15e584e9", size = 10575645, upload-time = "2026-10-29T14:09:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/f69aba8b22539e5ed58323d4a6ee974afd9edda238489c73926cc0fc9d51/maturin-1.16.0-py3-none-win_arm64.whl", hash = "sha256:95f34ce53249fb7095ff4f2aff64d2562c511d6edf3e216c15dbffcd82b72b4f", size = 9886475, upload-time = "2026-10-29T14:09:40.603Z" }, ] [[package]] @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -234,156 +234,160 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.50.1" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/db/c9cc34707ba2c23ce90d54e8a4ee9307583ad054b9df2ce570ba550cb576/pydantic_core-2.50.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b281a3b0f0822618fe5e3e0d8a2048b6356b14388505dc9374ccffeb69989713", size = 2154222, upload-time = "2026-10-11T18:31:38.345Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/6963f4ee55b7df7658f5c036c0f90baf8cd3d413f5d8b551f227e8467afc/pydantic_core-2.50.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa4c8bc12c1354c5550c0c35c1852c8c1901e89e06561724e03f8d0342e1f87", size = 2048126, upload-time = "2026-10-11T18:31:41.061Z" }, - { url = "https://files.pythonhosted.org/packages/bf/be/f75637e222085d29a3076edbc5cfd0dd9b77d145e310ff7dc1150f07558a/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa9de446b793de2beb6fa2d9d0961803126c4e2a99c2f25ab59b9fd6ea125c0", size = 2052460, upload-time = "2026-10-11T18:31:42.321Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/3d192ffa39ce3bd0e98e988fb5c055cfbf4af6e6e155a4ca5baa86d1f2cc/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de531ce1e2a3364e8767878b58f4ff728a434b4fde089781fe30b1e08e2396e0", size = 2190750, upload-time = "2026-10-11T18:31:43.572Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/12ea94df5220ab316ee5bb498ebcf710cff2db352653039fb6033f6997d1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c58106de36ac6a56314182958de20db8d3a29dfd5db527192cc754e4f8e7fb", size = 2390043, upload-time = "2026-10-11T18:31:44.745Z" }, - { url = "https://files.pythonhosted.org/packages/f6/bc/4fbb6dc90af038628ec824434a9841be9d46854f9267bb658f09e315024d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b4c3df25bd323bf1d36a648d563cf1fc69d717451569927151bdad7cad07a77", size = 2432397, upload-time = "2026-10-11T18:31:46.186Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1a/21a58791021397d9ca35295e3f4291c16e319155c5904334b89eb07e97e1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77ac30b19221cd9bd3fcfa3d4614eff93140d0572ab730cded17b64adca05f3", size = 2160719, upload-time = "2026-10-11T18:31:47.557Z" }, - { url = "https://files.pythonhosted.org/packages/73/6a/d5b2c5876162fd2c444eaed0064b5d9d9d0ef0d31530fc6a7997e8685ad8/pydantic_core-2.50.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d939de9c82e2126f7f48a7e658f8a85ed46d57662d53f44c49b8895fe94a3eb7", size = 2256164, upload-time = "2026-10-11T18:31:48.797Z" }, - { url = "https://files.pythonhosted.org/packages/af/e8/1a6625d6e99250388dd0ae817c5d75839cd6103e7b790f59e0ffff74955d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:30ddf019d082c117b5d309e5b86710c2a78909907ec1a9381feec3eec02eca0b", size = 2271176, upload-time = "2026-10-11T18:31:49.987Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/bbdd137d7ec41284701dab4d1489ea597d401dff2f5f3e6b41ea872e0b12/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ab756b72bd5054e4c7ef3ded331b35786cbd3cf931531a508f79a9537517064", size = 2304637, upload-time = "2026-10-11T18:31:51.251Z" }, - { url = "https://files.pythonhosted.org/packages/49/5d/7bd322e649b41b45130bc00bfa34dc57498c853aa8e6fc9604e33bc7087c/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:b087b1c5be7ac687cf22eabfe4b6b608d40df23610651e93611e1f49118baf84", size = 2467374, upload-time = "2026-10-11T18:31:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/53/48/41a91111590eb7ba68a7c64c03139235a4b8349930abcb87931aa80da796/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a44101320cfe99432db74237545a63057dc7a88dfe792cbcad0647f2af56cb81", size = 2480832, upload-time = "2026-10-11T18:31:54.06Z" }, - { url = "https://files.pythonhosted.org/packages/2a/02/4ac93a3d3f0ff412df1a36a9619743336fe6eec68c0df8a914163a3d22b4/pydantic_core-2.50.1-cp310-cp310-win32.whl", hash = "sha256:a4aaaa791bdae1c972a7e81765f4f3571c926b8e0b9b6e47346499fb80079665", size = 2123154, upload-time = "2026-10-11T18:31:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ba/3a7af3dcd480c4739aa82cd9fedee79a5531a7c553346abcf095bd05f0de/pydantic_core-2.50.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eedf82ee4753cdab8e50044c6bd569577eebc3859b11fecf4eb9223761ff966", size = 2179366, upload-time = "2026-10-11T18:31:56.697Z" }, - { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, - { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, - { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, - { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, - { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, - { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, - { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, - { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, - { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, - { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, - { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, - { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, - { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, - { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, - { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, - { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, - { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, - { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, - { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, - { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, - { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, - { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, - { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, - { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, - { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, - { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, - { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, - { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, - { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, - { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, - { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, - { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, - { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, - { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, - { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, - { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, - { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, - { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, + { url = "https://files.pythonhosted.org/packages/00/46/5fabd45007e53349fa9ff22c95968cb451acbc7d912dd8a9980ffa2325c1/pydantic_core-2.50.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6258754c473b3045a7aa30b833a8a331a1ab36c6147cb230789861d80bb26f83", size = 2163662, upload-time = "2026-10-29T16:31:28.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d5/6ec0fb1ccf75a8d6b42164f8e9440012416546f9badb683850c58167a5a8/pydantic_core-2.50.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a786cf782ebb2e45a43c7118d75d452aac982b79577dfe804d8dbf2878f0b589", size = 2119060, upload-time = "2026-10-29T16:31:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/54/39be898ef4811df54687e71f1bdb77095c7d792c36f98b20027602ab012a/pydantic_core-2.50.2-cp310-cp310-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:ea71b94a2eb5d2fc102926321f56d2ee8eddfad2673eadb1d23e9f287a492715", size = 2276387, upload-time = "2026-10-29T16:31:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9d/99fcf55e44eab0436cf0e49e34eee7393a9f737ab830018d67cdda7a02bc/pydantic_core-2.50.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:71615edeaf1ae70f5f3f558426072eede0168b7e5190fd9633878e29a0b85732", size = 2119441, upload-time = "2026-10-29T16:31:32.577Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fe/b51d611621963172900b8c2a6242c2d7a1ffb2a2026f5798449da6b211f3/pydantic_core-2.50.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1a5e859f5a8a6d834b0a13acaa0eaddf1a9b4b015fcda13bab7b471e18299d32", size = 2206739, upload-time = "2026-10-29T16:31:33.758Z" }, + { url = "https://files.pythonhosted.org/packages/95/08/cd920fdfd19d32aa93a815bd81a43cb892cf38ce96420157092935e1633c/pydantic_core-2.50.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d71a9a9352acbcb5c73d39dd003383599428067b6eda5cf7c5d335a7a0da37e1", size = 2394581, upload-time = "2026-10-29T16:31:35.306Z" }, + { url = "https://files.pythonhosted.org/packages/be/cf/40f2b1a7c5cee35a59fac898bc39e43283547cdedf4ffe152f1504104843/pydantic_core-2.50.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bf7f99a0e6616cea754e2334d129091f3260d011369d9b8c658522bfb2427619", size = 2446325, upload-time = "2026-10-29T16:31:36.498Z" }, + { url = "https://files.pythonhosted.org/packages/30/b0/5aae06bbe58fac7cf5c6aac6dc11b04a9beeb5e50bd74f6e4f724ac91c4e/pydantic_core-2.50.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c12a5a080570a91bbcbf6d10db008b80e3f5dea015ef2f0137bbec70f24f3f3", size = 2238710, upload-time = "2026-10-29T16:31:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/33/41/fa31929c6d3b9c91048abd486ac94ceb402a8a3e145b5abf1ef87300b781/pydantic_core-2.50.2-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:89213368eef163350af37127939ace099010e8102757905c3e329f1175950ed8", size = 2274879, upload-time = "2026-10-29T16:31:39.349Z" }, + { url = "https://files.pythonhosted.org/packages/27/00/f9cf3893c6a4c5f785888c3842d837a01067f4795978c6d56c798d0434ca/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d93c066a38f83376b5bc020e563666784d1b680a70d6ceb625fdb541af7a312e", size = 2313505, upload-time = "2026-10-29T16:31:40.742Z" }, + { url = "https://files.pythonhosted.org/packages/9a/48/c780d27b9d622301bac0700746ceb65f8e647d8319b509fea3acdf85b329/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:eec187d2876abb6a74203efbe2aa796455c1809334acf4e739dde35da8cfb69f", size = 2483718, upload-time = "2026-10-29T16:31:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fe/8acdea280a53ac5e9fefdcd6b4f01fed612980029d343d0202c044e245a3/pydantic_core-2.50.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:701bc55d08c22f981973ca4f6fdf4ea102ee1ee3a21b818b66fdb161ea711a4a", size = 2490576, upload-time = "2026-10-29T16:31:43.836Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b7/48d74ed20d415e43ead485f7b149270afa98fe6acf3d4c895100c9332073/pydantic_core-2.50.2-cp310-cp310-win32.whl", hash = "sha256:7f9821dfc0c473c96ad55ce4189c7cb4f9c94e7d4f563afb874173243dec1162", size = 2123629, upload-time = "2026-10-29T16:31:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/77d3eca0db83968efe5142dce85fdf050cf4b3a3b1e92ea3eb7525cbe143/pydantic_core-2.50.2-cp310-cp310-win_amd64.whl", hash = "sha256:3939538e640c94ab779a05f2d45dafedd01d65d93e1290797f3ac5b59fcb6455", size = 2242044, upload-time = "2026-10-29T16:31:46.664Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/1043d3840b7859cc19abede795e058a785ebc1fa011b022b0a69514dd5c5/pydantic_core-2.50.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2115d7ca6d58554af8a14da3dd32d361a4fe999fae52f115a6155679b5b00e72", size = 2160886, upload-time = "2026-10-29T16:31:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/6211dfe1794bde5ce14f3de2212551196784c67411ac5249d8aa627e3e23/pydantic_core-2.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2dd65864aad38dbcc57c3d0e2c978d4286e063378137d8fc0c9fcd558943285", size = 2115730, upload-time = "2026-10-29T16:31:49.449Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/50a1a9721cfc21d360916754096c90467fa21386ef16875445ea06a4cf6e/pydantic_core-2.50.2-cp311-cp311-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:26b778d4ebb75f181d36220ff0f54a513fc46f053d9b09ce79993c2f00bc45cc", size = 2274234, upload-time = "2026-10-29T16:31:50.668Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/22349c3114a3b3c400333ba6ec1d2e127fc9a4e4682440d8e23d954df0d1/pydantic_core-2.50.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e80c47314c7233e7105a7efcfffd89d56e479e558189d1959eeeaf2639a27680", size = 2116001, upload-time = "2026-10-29T16:31:51.887Z" }, + { url = "https://files.pythonhosted.org/packages/d8/2e/3b88612239e87cfddd370362e93b18a50d5075883817d6b35e8e3871af75/pydantic_core-2.50.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3bfe12243b3ded517c6734d91f127489e2f51bf95cb533b53130fe360333db80", size = 2204318, upload-time = "2026-10-29T16:31:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/a735bf3967d0fd73b512705d6f80fbe179d3b6208779721e86613cdf736d/pydantic_core-2.50.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:01eb0282d0b3a065d55956d1be9ce398c46db3dd6d277a6cb2358c8aa8c53179", size = 2392365, upload-time = "2026-10-29T16:31:54.721Z" }, + { url = "https://files.pythonhosted.org/packages/9d/16/0a0febc06b311d6ca43dd7fa068273d2c4d3c4ad86ac4a6c2f2cbe924c23/pydantic_core-2.50.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:196b87b48b2695426920e8470d20f9e99f019ebd86f15808aa018b68086840cd", size = 2443963, upload-time = "2026-10-29T16:31:56.455Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/22bd04c22adae266ae5e99e4c34bd882daf6c93c35bda2f9674421c1e7c3/pydantic_core-2.50.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86e7abd24a67f61a4bfe8df059b67a2bdd8bf33705178e98463195d140bd3780", size = 2236349, upload-time = "2026-10-29T16:31:57.837Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/77b12031179047a292128eee12cd7aad899d82391157356a581117e232e7/pydantic_core-2.50.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:aff31602e8518594e6f6bc670cdb27e37cb7dbeb2d5d0b76a7707b70fe951ddf", size = 2272702, upload-time = "2026-10-29T16:31:59.287Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/41f674077b08b79bf2f2ae3c777a5b7b8758647433c6c621580b612e8c1e/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a88851e8e332588cb8cced8b5318d1163c7d0bb87830aa8dcafbc4ae9602b73c", size = 2311296, upload-time = "2026-10-29T16:32:00.722Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/527d1d29cfb8efc7c7f9ed9f35ab5b16a33b1837b186c6e42ecbca699817/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3b1bb3950e0794a795c9344d818d5b476189b0c9c9755e8eb8fde87c3acee17a", size = 2481042, upload-time = "2026-10-29T16:32:02.423Z" }, + { url = "https://files.pythonhosted.org/packages/57/35/79877aaf083424e67c870ba07c0b5ed36a2096a0cfc8b058c1d921797723/pydantic_core-2.50.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:928a44ad1e21a7e923eb924d601605f0694cd187d2ce5d40bf7ad76650f4627d", size = 2488007, upload-time = "2026-10-29T16:32:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a627e40690c21be388c4abfddaefbf589e359614456156c90432ac0a76a/pydantic_core-2.50.2-cp311-cp311-win32.whl", hash = "sha256:9a87744e683d9a13f60b7fa5242c17f8d283ffa94ad727cb19063b48c3d34960", size = 2121376, upload-time = "2026-10-29T16:32:05.053Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9b/d21478003682505aad9619ff9e086bd29aced2615b23d5315cb37de14cad/pydantic_core-2.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:b958c217c6e56bef261f516e4caf5d490b079ad6a428752afada5a764cca6c71", size = 2238388, upload-time = "2026-10-29T16:32:06.811Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/670a52ea740fd60f5e0a873b74505714c4bfd04c4344f789310005c4cbbd/pydantic_core-2.50.2-cp311-cp311-win_arm64.whl", hash = "sha256:b0e6a2152706d84f7da8b9d34dcecc7e9bde678af32c40bd5b405f427ae0fbdb", size = 2185763, upload-time = "2026-10-29T16:32:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/82/ec0384d2a74415aeef7fcd032d29af84a98da0ca25cfb4937b10a2ac78eb/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:adec010c824290d4ca87047e8d8bfa4f85ac29f545f326cd5321696805d311ed", size = 2154528, upload-time = "2026-10-29T16:34:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/46483477921934aa31eec9cb86ba95d2f620f4c71da749024c2e3b6e8203/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:f47c1349c3b4274aeb18459d54244881a5c0dee0fdccedffca45704ba5c9e0a0", size = 2072173, upload-time = "2026-10-29T16:34:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/4a/16/c1eb6bdd67c9d5f42b290abb86948d8de288bdf4aeca06c588c3cfe6100b/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ee64baeab403b9de9c92846c5c1abec28ffb046cbc023bfae059897841f88af", size = 2116695, upload-time = "2026-10-29T16:34:31.103Z" }, + { url = "https://files.pythonhosted.org/packages/ae/67/ccd8294730d9335c687facbdfd77515fa3b323e9bbb042c5d63addc6f5f8/pydantic_core-2.50.2-graalpy311-graalpy242_311_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16ec8e07c37f02dcf68939cc7f202ba9a148cc155ee2acf22d337e10b24c2cb7", size = 2263561, upload-time = "2026-10-29T16:34:32.901Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, + { url = "https://files.pythonhosted.org/packages/18/9b/d240307fd6882a93a88bf038ff07e1a6dcac0e88f41951fe92c9b614f0b6/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b583ddeee6a6bb5bbc04462167c68034913221139d52d9f155ca6d1637a2fdcf", size = 2157909, upload-time = "2026-10-29T16:34:40.355Z" }, + { url = "https://files.pythonhosted.org/packages/30/e5/d336fb654f8f1584b4a20d71f92c2a966419bc5eafffd36848d8cb1a916d/pydantic_core-2.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5ab5738c39b25b5dc5a721270e2269a2f5e86e614375e6c07ee6caab8cbc0f2f", size = 2075773, upload-time = "2026-10-29T16:34:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/18203f64a405f11c3e4d569c9f49ad7a0b5901cd5fa2dff2436d28531c60/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21a902e546032c82b7275552345a2342d862e0541aea7226c91067c396e68b3f", size = 2277290, upload-time = "2026-10-29T16:34:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c4/a4e0ae45ad1554c38caa93e200fdd08e74d2c0149b4242c78b2475903379/pydantic_core-2.50.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92ada606285dab054e40848a951f583c537cde1e54701ea1305ec0096877012d", size = 2265597, upload-time = "2026-10-29T16:34:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/99/20/304baf7724ac137b921ce793e4b686cf4cddc431da4888e5419e7bc7374f/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1b65a42afc73b39e6ae908ec7b9c3b9898676a7b15ce4c66d4965f7071007c8", size = 2289092, upload-time = "2026-10-29T16:34:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/0fe5ac160718d75f2f9729bb31aa9a349dadd0d32886b8c0ecc253629c46/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c324a7e0c0012e3974053f81a270d8f1aa23227571f4ddd3254c911bb356459b", size = 2458417, upload-time = "2026-10-29T16:34:46.827Z" }, + { url = "https://files.pythonhosted.org/packages/43/44/042cbd7e46f00f45aea36496ca8bdb6b1136c2ec4063875a25286c2ba8fc/pydantic_core-2.50.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:cb8d2d7b07cbe2d7488de7e53671f836c4f4a8032dcda079df33dc1b7e2397c0", size = 2492296, upload-time = "2026-10-29T16:34:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8e/e8aef16e587215a1795dbe991e41006003aef1f1b6f554361f3a6edd585a/pydantic_core-2.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7c6e1bd6cadae13958e52897ef020e85c9916a95917149fca463bb0c0421a7c6", size = 2323090, upload-time = "2026-10-29T16:34:50.008Z" }, + { url = "https://files.pythonhosted.org/packages/21/36/d083f973e28c5718f9d9d819c0b8171df1526124e8c3709bd55ddf1d4244/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:f078e276848bc23d1f92f46fc88d5b9cbb824b9acc60d9e07116620e502e16a9", size = 2157909, upload-time = "2026-10-29T16:34:51.579Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4b/e2eb5b51f46411ceb80451a7bf4aee215fd84012eeba22a183b4699006b5/pydantic_core-2.50.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3435c443da7510a6de1a30c68beae338852a11f0440cc8320ea756a162f38dbe", size = 2075771, upload-time = "2026-10-29T16:34:53.469Z" }, + { url = "https://files.pythonhosted.org/packages/d5/99/e9703785468d1e644f19d602985b722ff97cc8ca67389e2d79f8fb562cfa/pydantic_core-2.50.2-pp311-pypy311_pp80-manylinux_2_28_x86_64.whl", hash = "sha256:b5675a12a7b67063fe3ca187e735b0463cf13fabc10166ae4d4b2f775fd0632d", size = 2265708, upload-time = "2026-10-29T16:34:54.662Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d7/079d3d36aa6ee0333847d968d91ff185837541741d3c58ed0316103309bf/pydantic_core-2.50.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:f5ba7ef10ce82737e173fa95cafa1c8210918f7e653715b2c7a7a31877a74848", size = 2323087, upload-time = "2026-10-29T16:34:56.176Z" }, ] [[package]] @@ -410,7 +414,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -421,22 +425,22 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] name = "pytest-benchmark" -version = "5.3.0" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "py-cpuinfo2" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/8f/83a15e40dbc34a580ee56eb56983cae5394c6e94d50cf28fe268e457be25/pytest_benchmark-5.3.0.tar.gz", hash = "sha256:358444d4e89be901ee2b6404fb043ac3d7684002ad7f3563cc153fca6339c965", size = 375410, upload-time = "2026-08-23T17:45:08.891Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/d1/1bc61155de46f25473a06476ad1b3a94f96d40d49cfa98f82c1184117808/pytest_benchmark-5.3.1.tar.gz", hash = "sha256:c1ee70a58f9bfb1ac955cc634247a340b0df7bcb949071cfca396f16c6fc3516", size = 385467, upload-time = "2026-11-01T03:32:10.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/42/7e80f7cfa191e0a766d1de99b4661847415ad5db34f8209d81fd42175b59/pytest_benchmark-5.3.0-py3-none-any.whl", hash = "sha256:920ab1dfcffa718d49aa15ba144c7e357bda59216a0dc308016cc1c7236f719d", size = 48401, upload-time = "2026-08-23T17:45:07.094Z" }, + { url = "https://files.pythonhosted.org/packages/78/75/919ac85ad3f762e45aa369d2f68d40390e316474b6bb8434aeb6e3c8bf8b/pytest_benchmark-5.3.1-py3-none-any.whl", hash = "sha256:d7732a9952b1c4a4a32277adc693739126eab37afc3a711847f53805ccadc020", size = 49142, upload-time = "2026-11-01T03:32:08.359Z" }, ] [[package]] From cc94c9ccba1100c39f9764e8511040bbb34be249 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Thu, 5 Nov 2026 17:34:36 +0100 Subject: [PATCH 316/322] fix(all): pass defaults for DefaultValue fields to ctors (#5267) --- src/Fable.Transforms/Beam/Fable2Beam.fs | 7 ++- src/Fable.Transforms/FSharp2Fable.fs | 31 ++++++++++++ src/Fable.Transforms/QuotationEmitter.fs | 6 ++- src/Fable.Transforms/Rust/Fable2Rust.fs | 1 + src/fable-library-ts/Util.ts | 19 ++++++++ tests/Beam/TypeTests.fs | 52 ++++++++++++++++++++ tests/Js/Main/QuotationTests.fs | 8 ++++ tests/Js/Main/TypeTests.fs | 60 ++++++++++++++++++++++++ tests/Python/TestType.fs | 57 ++++++++++++++++++++++ tests/Rust/tests/src/QuotationTests.fs | 6 +++ tests/Rust/tests/src/TypeTests.fs | 52 ++++++++++++++++++++ 11 files changed, 297 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/Beam/Fable2Beam.fs b/src/Fable.Transforms/Beam/Fable2Beam.fs index afa3b7c3ef..92b36b83f1 100644 --- a/src/Fable.Transforms/Beam/Fable2Beam.fs +++ b/src/Fable.Transforms/Beam/Fable2Beam.fs @@ -1646,7 +1646,12 @@ and transformValue match com.TryGetEntity(ref) with | Some entity -> let fieldEntries = - List.zip (entity.FSharpFields |> List.map (fun f -> f.Name)) values + let fieldNames = + entity.FSharpFields + |> List.filter (fun f -> not f.IsStatic) + |> List.map (fun f -> f.Name) + + List.zip fieldNames values |> List.map (fun (name, value) -> let fieldKey = if ctx.ClassFieldPrefix then diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 724142ae23..37e6eb62d8 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -741,6 +741,35 @@ let private transformUnionCaseTest return Fable.Test(unionExpr, Fable.UnionCaseTest(tag), r) } +let private withDefaultValueFields (com: IFableCompiler) (ctx: Context) (fsType: FSharpType) (args: Fable.Expr list) = + let ent = fsType.TypeDefinition + + let isDefaultValueField (field: FSharpField) = + not field.IsStatic && hasAttrib Atts.defaultValue field.FieldAttributes + + let fields = + ent.FSharpFields |> Seq.filter (fun field -> not field.IsStatic) |> List.ofSeq + + if not ctx.CapturingQuotation && fields |> List.exists isDefaultValueField then + let genArgs = + Seq.zip ent.GenericParameters (getGenericArguments fsType) + |> Seq.map (fun (param, typ) -> genParamName param, makeType ctx.GenericArgs typ) + |> Map.ofSeq + + let rec fill (fields: FSharpField list) args = + match fields, args with + | field :: fields, _ when isDefaultValueField field -> + let value = + makeType genArgs field.FieldType |> Replacements.Api.defaultof com ctx None + + value :: fill fields args + | _ :: fields, arg :: args -> arg :: fill fields args + | _, args -> args + + fill fields args + else + args + let rec private transformDecisionTargets (com: IFableCompiler) (ctx: Context) @@ -1594,6 +1623,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs let r = makeRangeFrom fsExpr let! baseValue = transformExpr com ctx [] first let! ownArgs = transformExprList com ctx ownArgs + let ownArgs = withDefaultValueFields com ctx fsType ownArgs let baseIdent = getIdentUniqueName com ctx "base" |> makeTypedIdent baseValue.Type @@ -1636,6 +1666,7 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.NewRecord(fsType, argExprs) -> let r = makeRangeFrom fsExpr let! argExprs = transformExprList com ctx argExprs + let argExprs = withDefaultValueFields com ctx fsType argExprs let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) let entRef = FsEnt.Ref fsType.TypeDefinition diff --git a/src/Fable.Transforms/QuotationEmitter.fs b/src/Fable.Transforms/QuotationEmitter.fs index 39dbb6add7..dcc3d22d4a 100644 --- a/src/Fable.Transforms/QuotationEmitter.fs +++ b/src/Fable.Transforms/QuotationEmitter.fs @@ -434,7 +434,11 @@ and private emitQuotedValue (com: Compiler) (bound: Set) (kind: ValueKin | NewRecord(values, entRef, _genArgs) -> let fieldNames = match com.TryGetEntity(entRef) with - | Some ent -> ent.FSharpFields |> List.map (fun f -> makeStrConst f.Name) |> makeArray Any + | Some ent -> + ent.FSharpFields + |> List.filter (fun f -> not f.IsStatic && not f.HasDefaultValueAttribute) + |> List.map (fun f -> makeStrConst f.Name) + |> makeArray Any | None -> makeArray Any [] let emittedValues = diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index efeb1566ae..887d72d73f 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -4364,6 +4364,7 @@ module Util = let getEntityFieldsAsIdents com (ent: Fable.Entity) : Fable.Ident list = let fieldIdents = ent.FSharpFields + |> List.filter (fun field -> not field.IsStatic) |> List.map (fun field -> let name = field.Name let typ = FableTransforms.uncurryType field.FieldType diff --git a/src/fable-library-ts/Util.ts b/src/fable-library-ts/Util.ts index 484846d3e7..382a5c1d25 100644 --- a/src/fable-library-ts/Util.ts +++ b/src/fable-library-ts/Util.ts @@ -834,6 +834,7 @@ export function uncurry2(f: (a1: T1) => (a2: T2) => TResult) { } export function curry2(f: (a1: T1, a2: T2) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as ((a1: T1) => (a2: T2) => TResult) ?? ((a1: T1) => (a2: T2) => f(a1, a2)); } @@ -845,6 +846,7 @@ export function uncurry3(f: (a1: T1) => (a2: T2) => (a3: T3 } export function curry3(f: (a1: T1, a2: T2, a3: T3) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => f(a1, a2, a3)); } @@ -859,6 +861,7 @@ export function uncurry4( } export function curry4(f: (a1: T1, a2: T2, a3: T3, a4: T4) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => f(a1, a2, a3, a4)); } @@ -873,6 +876,7 @@ export function uncurry5( } export function curry5(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => f(a1, a2, a3, a4, a5)); } @@ -887,6 +891,7 @@ export function uncurry6( } export function curry6(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => f(a1, a2, a3, a4, a5, a6)); } @@ -901,6 +906,7 @@ export function uncurry7( } export function curry7(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => f(a1, a2, a3, a4, a5, a6, a7)); } @@ -915,6 +921,7 @@ export function uncurry8( } export function curry8(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => f(a1, a2, a3, a4, a5, a6, a7, a8)); } @@ -929,6 +936,7 @@ export function uncurry9( } export function curry9(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9)); } @@ -943,6 +951,7 @@ export function uncurry10( } export function curry10(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10)); } @@ -957,6 +966,7 @@ export function uncurry11 } export function curry11(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11)); } @@ -971,6 +981,7 @@ export function uncurry12(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12)); } @@ -985,6 +996,7 @@ export function uncurry13(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13)); } @@ -999,6 +1011,7 @@ export function uncurry14(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14)); } @@ -1013,6 +1026,7 @@ export function uncurry15(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)); } @@ -1027,6 +1041,7 @@ export function uncurry16(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16)); } @@ -1041,6 +1056,7 @@ export function uncurry17(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17)); } @@ -1055,6 +1071,7 @@ export function uncurry18(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18)); } @@ -1069,6 +1086,7 @@ export function uncurry19(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)); } @@ -1083,6 +1101,7 @@ export function uncurry20(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9, a10: T10, a11: T11, a12: T12, a13: T13, a14: T14, a15: T15, a16: T16, a17: T17, a18: T18, a19: T19, a20: T20) => TResult) { + if (f == null) { return null as never; } return getCurried(f) as (a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => TResult ?? ((a1: T1) => (a2: T2) => (a3: T3) => (a4: T4) => (a5: T5) => (a6: T6) => (a7: T7) => (a8: T8) => (a9: T9) => (a10: T10) => (a11: T11) => (a12: T12) => (a13: T13) => (a14: T14) => (a15: T15) => (a16: T16) => (a17: T17) => (a18: T18) => (a19: T19) => (a20: T20) => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)); } diff --git a/tests/Beam/TypeTests.fs b/tests/Beam/TypeTests.fs index 51a3d08ffb..0160ed16d0 100644 --- a/tests/Beam/TypeTests.fs +++ b/tests/Beam/TypeTests.fs @@ -60,6 +60,32 @@ type Point2D = new(xy: float) = { X = xy; Y = xy } end +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + exception MyEx of int * string type MyEx2(f: float) = @@ -652,6 +678,32 @@ let ``test Other Value Types work`` () = let p = Point2D(2.) p.Y |> equal 2. +[] +let ``test Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``test Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + [] let ``test Struct with mutable fields works`` () = let x = ValueTypeR(-10.0) diff --git a/tests/Js/Main/QuotationTests.fs b/tests/Js/Main/QuotationTests.fs index 9e603a7b20..ce5dd64b39 100644 --- a/tests/Js/Main/QuotationTests.fs +++ b/tests/Js/Main/QuotationTests.fs @@ -16,6 +16,8 @@ type QuotationTestUnion = let inline quotTestDouble x = x * 2 +type QuotationDefaultValueRecord = { A: int; [] mutable B: int; C: string } + type QuotationIndexerTest(values: int[]) = member _.Item with get (i: int) = values.[i] @@ -321,4 +323,10 @@ let tests = | Lambda(_, Var _) -> "Var" | _ -> "?") |> equal "Var" + + testCase "NewRecord leaves out DefaultValue fields" <| fun () -> + (match <@ { A = 1; C = "c" }: QuotationDefaultValueRecord @> with + | NewRecord(_, args) -> args |> Seq.map (function Value(v, _) -> string v | _ -> "?") |> String.concat "," + | _ -> "other") + |> equal "1,c" ] diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 76604f5c73..4f8b8e5bde 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -439,6 +439,33 @@ type ClassWithDefaultValueStaticFields() = static member Decimal = ClassWithDefaultValueStaticFields.decimal static member StructTuple2 = ClassWithDefaultValueStaticFields.structTuple2 +type ClassWithDefaultValueInstanceFields = + val X: int + [] val mutable D: int + [] val mutable S: string + val Y: string + new (x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] val mutable D: int + val Y: string + new (x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] static val mutable private count: int + val X: int + [] val mutable D: int + val Y: int + new (x, y) = { X = x; Y = y } + static member Count with get () = ClassWithDefaultValueStaticAndInstanceFields.count and set v = ClassWithDefaultValueStaticAndInstanceFields.count <- v + +type ClassWithDefaultValueFunctionField = + val F: int -> int -> int + [] val mutable G: int -> int -> int + new (f) = { F = f } + [] type NestedStruct = @@ -1848,6 +1875,39 @@ let tests = ClassWithDefaultValueStaticFields.IncrCount() |> equal 1 + testCase "Instance [] fields are skipped by explicit constructors" <| fun () -> + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + + testCase "Explicit constructors skip static fields next to DefaultValue fields" <| fun () -> + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + ClassWithDefaultValueStaticAndInstanceFields.Count <- 4 + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + ClassWithDefaultValueStaticAndInstanceFields.Count |> equal 4 + + testCase "Function-typed DefaultValue fields are null" <| fun () -> + let c = ClassWithDefaultValueFunctionField(fun a b -> a + b) + isNull (box c.G) |> equal true + c.F 2 3 |> equal 5 + c.G <- fun a b -> a * b + c.G 2 3 |> equal 6 + testCase "Unchecked.defaultof works for fields on structs" <| fun () -> let top = TopLevelStruct() top.A |> equal 0 diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index cad1b1c9e3..a2eef539b1 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -369,6 +369,35 @@ type TypeWithDefaultValueTest() = [] val mutable ObjValue: System.Collections.Generic.Dictionary +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + static member Count + with get () = ClassWithDefaultValueStaticAndInstanceFields.count + and set v = ClassWithDefaultValueStaticAndInstanceFields.count <- v + [] type NestedStruct = val A: float @@ -1840,6 +1869,34 @@ let ``test Different type parameter combinations work correctly`` () = let result3 = getTwoValues() result3 |> equal ("B", "C") +[] +let ``test Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``test Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + ClassWithDefaultValueStaticAndInstanceFields.Count <- 4 + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + ClassWithDefaultValueStaticAndInstanceFields.Count |> equal 4 + [] let ``test Unchecked.defaultof works for fields on structs`` () = let top = TopLevelStruct() diff --git a/tests/Rust/tests/src/QuotationTests.fs b/tests/Rust/tests/src/QuotationTests.fs index 6a3312a302..9f9172ae8c 100644 --- a/tests/Rust/tests/src/QuotationTests.fs +++ b/tests/Rust/tests/src/QuotationTests.fs @@ -211,6 +211,8 @@ let ``Evaluate list quotation`` () = type private MyRec = { X: int; Y: int } +type private MyDefaultValueRec = { A: int; [] mutable B: int; C: int } + let private newUnionCaseName (e: Expr) = match e with | NewUnionCase(uci, _args) -> uci.Name @@ -247,6 +249,10 @@ let ``NewUnionCase exposes the tag`` () = let ``NewRecord exposes the argument count`` () = newRecordArgCount <@ ({ X = 1; Y = 2 }: MyRec) @> |> equal 2 +[] +let ``NewRecord leaves out DefaultValue fields`` () = + newRecordArgCount <@ ({ A = 1; C = 2 }: MyDefaultValueRec) @> |> equal 2 + // --- TupleGet evaluation (B3) --- // A tuple-deconstructing let lowers to a NewTuple bound to a var, then TupleGet nodes // pulling out each element. Evaluating it exercises both the ExprNewTuple arm (builds a diff --git a/tests/Rust/tests/src/TypeTests.fs b/tests/Rust/tests/src/TypeTests.fs index f7855de36e..42e093d1da 100644 --- a/tests/Rust/tests/src/TypeTests.fs +++ b/tests/Rust/tests/src/TypeTests.fs @@ -288,6 +288,32 @@ type Point2D = new(xy: float) = { X = xy; Y = xy } end +type ClassWithDefaultValueInstanceFields = + val X: int + [] + val mutable D: int + [] + val mutable S: string + val Y: string + new(x, y) = { X = x; Y = y } + +[] +type StructWithDefaultValueInstanceField = + val X: int + [] + val mutable D: int + val Y: string + new(x, y) = { X = x; Y = y } + +type ClassWithDefaultValueStaticAndInstanceFields = + [] + static val mutable private count: int + val X: int + [] + val mutable D: int + val Y: int + new(x, y) = { X = x; Y = y } + exception MyEx of int * string // type MyEx2(f: float) = @@ -1352,6 +1378,32 @@ let ``struct without explicit ctor works`` () = t1 |> equal t2 (compare t1 t2) |> equal 0 +[] +let ``Instance DefaultValue fields are skipped by explicit constructors`` () = + let c = ClassWithDefaultValueInstanceFields(1, "y") + c.X |> equal 1 + c.D |> equal 0 + c.S |> equal null + c.Y |> equal "y" + c.D <- 5 + c.S <- "s" + c.D |> equal 5 + c.S |> equal "s" + + let mutable s = StructWithDefaultValueInstanceField(2, "z") + s.X |> equal 2 + s.D |> equal 0 + s.Y |> equal "z" + s.D <- 6 + s.D |> equal 6 + +[] +let ``Explicit constructors skip static fields next to DefaultValue fields`` () = + let c = ClassWithDefaultValueStaticAndInstanceFields(1, 2) + c.X |> equal 1 + c.D |> equal 0 + c.Y |> equal 2 + [] let ``Struct with mutable fields works`` () = let x = ValueTypeR(-10.0) From 7e12c106644585279977fb39a46ae739115f11eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:37:46 +0100 Subject: [PATCH 317/322] chore: Bump the all-pip group with 5 updates (#5263) Signed-off-by: dependabot[bot] --- pyproject.toml | 10 +- uv.lock | 322 ++++++++++++++++++++++++------------------------- 2 files changed, 166 insertions(+), 166 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4cee6e5f80..2432421a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,14 +16,14 @@ Homepage = "https://fable.io" [dependency-groups] dev = [ - "pytest>=9.1.1,<10", - "ruff>=0.17.2,<0.18", - "maturin>=1.15.0,<2", + "pytest>=9.2.0,<10", + "ruff>=0.17.3,<0.18", + "maturin>=1.16.0,<2", "dunamai>=1.26.3,<2", - "pydantic>=2.14.1", + "pydantic>=2.14.2", "attrs>=26.1.0", "pyright>=1.1.414", - "ty>=0.0.91", + "ty>=0.0.95", ] [tool.hatch.build.targets.sdist] diff --git a/uv.lock b/uv.lock index 3a7a74917a..75f4140779 100644 --- a/uv.lock +++ b/uv.lock @@ -64,12 +64,12 @@ dev = [ dev = [ { name = "attrs", specifier = ">=26.1.0" }, { name = "dunamai", specifier = ">=1.26.3,<2" }, - { name = "maturin", specifier = ">=1.15.0,<2" }, - { name = "pydantic", specifier = ">=2.14.1" }, + { name = "maturin", specifier = ">=1.16.0,<2" }, + { name = "pydantic", specifier = ">=2.14.2" }, { name = "pyright", specifier = ">=1.1.414" }, - { name = "pytest", specifier = ">=9.1.1,<10" }, - { name = "ruff", specifier = ">=0.17.2,<0.18" }, - { name = "ty", specifier = ">=0.0.91" }, + { name = "pytest", specifier = ">=9.2.0,<10" }, + { name = "ruff", specifier = ">=0.17.3,<0.18" }, + { name = "ty", specifier = ">=0.0.95" }, ] [[package]] @@ -83,23 +83,23 @@ wheels = [ [[package]] name = "maturin" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/c8/22e5e21b2679c9bce6415ca578034ca2cc9316be0642ae21e051a2d5198c/maturin-1.15.0.tar.gz", hash = "sha256:94b26cc8e8aba61a5f2099715fe640e18c5f678e9a500408b38761263954228a", size = 385504, upload-time = "2026-08-24T12:11:22.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/71/eb078174335ff30bde1be2ad54fb6256db4b160eb3efa81163fa30f8c814/maturin-1.16.0.tar.gz", hash = "sha256:13e69ca12542999d8c5467131de72a49bb76e4c284b9a45c5783df7c0fcb3a8a", size = 396641, upload-time = "2026-10-29T14:09:43.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/69/5c01b461044eb1f45ddcce006706eb88110c793cdb11c7ae0b5e08492e94/maturin-1.15.0-py3-none-linux_armv6l.whl", hash = "sha256:6bf6dc62e22d4dcfd5a51244ff0d58975fa4979c48209fe84159617648956d82", size = 10206220, upload-time = "2026-08-24T12:10:53.327Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1f/2b431554e11687cdb1077e0cdadcc118c53f611086b3af00c8545a67c6a5/maturin-1.15.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:cd35772633f489841132bc8e71d6fc7f842df30b9c05cd5cdf1ee1ddcb744cc7", size = 19416513, upload-time = "2026-08-24T12:10:56.126Z" }, - { url = "https://files.pythonhosted.org/packages/51/36/e23a21cb34a648b711036b9b2fe1d4f3f4ee24f8db54215d73f1a9a3a3ec/maturin-1.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c40b4eae7bf5ef1f4b1af8d623fe4105016f93578fb15b764e741d08ec3b92dd", size = 10014962, upload-time = "2026-08-24T12:10:58.486Z" }, - { url = "https://files.pythonhosted.org/packages/8c/80/33b15cb2d8f30f12c807955e8f2fd775027692904e30ec0784744ce8cd83/maturin-1.15.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7eb066372f541f8eb4909c79c5d9bd0b9e8125980bdf1ec9e8aba23c6c8d6c55", size = 10196223, upload-time = "2026-08-24T12:11:00.696Z" }, - { url = "https://files.pythonhosted.org/packages/fe/91/b495e19e2f5c503b540452b2039115e7b2363867e8c5ad4179eb752fa92c/maturin-1.15.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e", size = 10541186, upload-time = "2026-08-24T12:11:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2b/2abff58037188d852b124871b1f0d720e1c2bfb3d4f1b03d87c52cd66488/maturin-1.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0ebf9767892725083138e671c34482c660317a2f3d6a29fc0e0f34e9d8c99136", size = 10083468, upload-time = "2026-08-24T12:11:05.012Z" }, - { url = "https://files.pythonhosted.org/packages/3f/07/b7e9f8be99a6627849e81ac7b6694876bce8f50a92995fe17e3cf2610f0a/maturin-1.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7ab7eebffd7b8debca2265985de4eaeb332141276d24b9560b5ad484d4b3add1", size = 10047786, upload-time = "2026-08-24T12:11:07.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ab/167e3cb7accee11b507dbe53e0e87aeccb376d44ae66284c96ee4df3a9fd/maturin-1.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:126e12e618b4db42f68c779a56d41f82a390145ba36ac3f621d057eb34f5ad9d", size = 13315332, upload-time = "2026-08-24T12:11:09.433Z" }, - { url = "https://files.pythonhosted.org/packages/14/4d/801379f646cbc6b00998e5289b0630a886be3a4ee4c75b6bc9b87478a7f1/maturin-1.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f9d33e6c3f9615c8caceecbbbd440f8eb25a3ddeb687077682cd5eca2e9ae15", size = 10807183, upload-time = "2026-08-24T12:11:11.73Z" }, - { url = "https://files.pythonhosted.org/packages/89/27/2e612e1cbd1580e9e94d4722c227b5180dca27b32b955a34b79918aa1292/maturin-1.15.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:bf29beddd0c6708f112db51d5275fc28b28b9e9c9c5faae387eaef662918b176", size = 10413274, upload-time = "2026-08-24T12:11:14.04Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/202a7b4d75a51f20f84ec9ce3b7345b12b822207072164e2b1c6ef665125/maturin-1.15.0-py3-none-win32.whl", hash = "sha256:da649988be98e87e009e51b1bf0d301b6a301bc0cecbdd60d40d8ba60748d1ca", size = 8928744, upload-time = "2026-08-24T12:11:16.269Z" }, - { url = "https://files.pythonhosted.org/packages/40/dc/4e90da594986ba78dd3bc8a5921ecdcdb11085b22b02a412caab3b225601/maturin-1.15.0-py3-none-win_amd64.whl", hash = "sha256:552c2be4afd43fe8d5c9f3ec8d4c4756d973b8dcbe94c14084390301f50243e1", size = 10335085, upload-time = "2026-08-24T12:11:18.326Z" }, - { url = "https://files.pythonhosted.org/packages/8b/10/15d4314edf130955edf2dc237aa393a8a7c10f2b9b57b89fa2f61f915659/maturin-1.15.0-py3-none-win_arm64.whl", hash = "sha256:c7dc0c66c78d3debdd9c5aa807e861fbcbf07f3505d34b125df74c03986b0f48", size = 9713795, upload-time = "2026-08-24T12:11:20.83Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f8/235d4a8b3d61af3c7744582ae9e6ccb8f096e9f68f06fe62d7d03e5d7b2d/maturin-1.16.0-py3-none-linux_armv6l.whl", hash = "sha256:deacbb204a20326862e194c7df4958e0514a0411c9a3ccf228a00067c11b5c7e", size = 10425070, upload-time = "2026-10-29T14:09:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/f49a90d8c489dc5f134654ec91d12a78a51599fdc8c2b5a1bc2391bca0ac/maturin-1.16.0-py3-none-macosx_10_12_universal2.macosx_10_12_x86_64.macosx_11_0_arm64.whl", hash = "sha256:131fcc17ddb3ccc48aa183d2172ebf994c322b858e1b9694fb8744d783df66f6", size = 19839189, upload-time = "2026-10-29T14:09:09.441Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8e/d65633d7c25484cb82735a51e15bc969f9874a13557e2b352d27df574c20/maturin-1.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2216d731c9324869d3bff72c0c73009bb6e640b3fa7d340174ae70dc874d1d9f", size = 10271659, upload-time = "2026-10-29T14:09:12.596Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dc/e77f490557cb5e045a75c3d18a7965425ac576235c970dfff4d903eb2c74/maturin-1.16.0-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:837db2f4d93d4899c3ac393fb3cc9eebf35b6002336e5d574f4a4fb6c49fe283", size = 10763361, upload-time = "2026-10-29T14:09:16.11Z" }, + { url = "https://files.pythonhosted.org/packages/fa/eb/535564a4211105fd64611fa4f9437c8925eded26a97d58de84565f5aebfa/maturin-1.16.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:399fadcd4af39532a173e010240c34930eacc1bb1746bb179863481c68bcf1ab", size = 10442856, upload-time = "2026-10-29T14:09:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/570e13ce81f103be6609c067a06c5bac08da5af0cf7ea700c9ae43419be9/maturin-1.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e285ec9eb10243d45d10417178cb2eb76e0cbb184d0aa4378b503489fa12ee78", size = 10298796, upload-time = "2026-10-29T14:09:21.385Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/26b40c31c354359fe4e49398113d018694c7035968fe1b569bb13c3905af/maturin-1.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:609228d639c5b888dc41364cbd318547a59f027481f53cba85838f46da4b3f02", size = 10268023, upload-time = "2026-10-29T14:09:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/faca1784d917c2eecd498648dff6a0179b299c8142cc448ea5b1183545ec/maturin-1.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7001fd3ac591820d01f2fa041552c367ec587c98649d9f10a817cc7f28e59062", size = 13739038, upload-time = "2026-10-29T14:09:26.662Z" }, + { url = "https://files.pythonhosted.org/packages/36/c1/d67e77aa07100e3b2d4c0d9ef6922646d9c4bab25cc04f91023aefc72418/maturin-1.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b0573a146803f24566f5b301a4572a8e49bcab4917175407c730da13b46e134", size = 11100193, upload-time = "2026-10-29T14:09:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/ccef52f02ab0e1f7258bc8fe0140b4991c6287458ef883af1027f946a253/maturin-1.16.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c16d3589cbddda9420237fdb1394b1a9b36483c0bce83b23de6118c908a8e940", size = 10663849, upload-time = "2026-10-29T14:09:32.183Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9b/3104f62dbb428f9e26e35cba2c80cfc42ba9bb1dc4968882c19d1bfe594b/maturin-1.16.0-py3-none-win32.whl", hash = "sha256:9e221c047931caa79283cc0c2278904ae1b25880dde3574b0447657d72907689", size = 9124894, upload-time = "2026-10-29T14:09:34.857Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/c25ce706c4797ee1663bcf1f386ffe91d8a3b3589700a135a204b49bc4b0/maturin-1.16.0-py3-none-win_amd64.whl", hash = "sha256:9d43a2adf51a2794e907a8f7dbad06eb114d5edb0909c73c1fa9040f15e584e9", size = 10575645, upload-time = "2026-10-29T14:09:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/f69aba8b22539e5ed58323d4a6ee974afd9edda238489c73926cc0fc9d51/maturin-1.16.0-py3-none-win_arm64.whl", hash = "sha256:95f34ce53249fb7095ff4f2aff64d2562c511d6edf3e216c15dbffcd82b72b4f", size = 9886475, upload-time = "2026-10-29T14:09:40.603Z" }, ] [[package]] @@ -131,7 +131,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -139,115 +139,115 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/d5/b4867fa7c7f04c34997d7d8aad744f64d210bb99d67cf18bf3fb1eea1aed/pydantic-2.14.2.tar.gz", hash = "sha256:fb7b2db6cefe8414dd612edeede49c4c433acb0432b0f92b95024e45f66c505d", size = 911596, upload-time = "2026-10-29T16:37:12.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/97/01/c71a71258b58659c72504e5fd65b451f6e36e34dc931d0b237ded213c814/pydantic-2.14.2-py3-none-any.whl", hash = "sha256:7efbf7507b937931cd32a5755acb00ea1cf5ccb1d4e8067a5850d5069e382118", size = 490403, upload-time = "2026-10-29T16:37:10.181Z" }, ] [[package]] name = "pydantic-core" -version = "2.50.1" +version = "2.50.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/8e/23ec1021834e35041fbb2f10946141e6acdfd39379c45cc4d65a4e8a1d98/pydantic_core-2.50.2.tar.gz", hash = "sha256:1b0e8dc3932cf4e9ed5727570325cacc716b9409ffc794a221c0ccc72e8eb775", size = 518711, upload-time = "2026-10-29T16:34:57.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, - { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, - { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, - { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, - { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, - { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, - { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, - { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, - { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, - { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, - { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, - { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, - { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, - { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, - { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, - { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, - { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, - { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, - { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, - { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, - { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, - { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, - { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, - { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, - { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, - { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, - { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, - { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, - { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, - { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, - { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, - { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, - { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, - { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, - { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/6609a66cda909d414c58438f9bd70b1e947fe12c25f6bd40c93121afd7ef/pydantic_core-2.50.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e3d824b6152e1c0fd58dc6266f2de470eb7b2286135294f5f7cd26a0f0693738", size = 2220341, upload-time = "2026-10-29T16:32:10.193Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/92463ae7be78d73885487f72c4599902c2cc190378bccaad5718b7495250/pydantic_core-2.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:596921489390a002d6a5e303384a0f1f98384f8bb550fbf38a1aafa6093da0f9", size = 2093701, upload-time = "2026-10-29T16:32:11.502Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/24147a1aa22f7eade546172750e1110fd7b2793998057809800d3ebeae4e/pydantic_core-2.50.2-cp312-cp312-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:36d8b9725684ca3bf658b90a101d335fc8cae405bb22196b7b5ef41344913756", size = 2253622, upload-time = "2026-10-29T16:32:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/f6/50/2628ca3c83a0d9a6fda3305fe59b1b28e40c0221e8f234cd1ef86fb64745/pydantic_core-2.50.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43a8fd56f3c681f3cda2d5c842ad28e88241df742dfcbe8fd1dd437a3056e1f6", size = 2114101, upload-time = "2026-10-29T16:32:14.702Z" }, + { url = "https://files.pythonhosted.org/packages/21/c8/cc6d7304a93f91f5e06260ccad71cc7d882489663a37dec009532379fc46/pydantic_core-2.50.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6f5a432c6c1ef18a1a02a04b3616b8d3f2f88b13faa46caae1ac6559ab202234", size = 2199541, upload-time = "2026-10-29T16:32:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/18f44ac5696b3e2b2384cb89c9fbe6ed69860f528e232b9fb46cc5b45a17/pydantic_core-2.50.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d3cd8be71c4da3c9b42cc92f17e5e3036bdca98bf18555a0e072c3fe87abbb4", size = 2368597, upload-time = "2026-10-29T16:32:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/5f2a48ef819a7e1b74fb003a85d27576ad2f9ddf3901b88998ac63ce1e00/pydantic_core-2.50.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a438cb8a3f87f2c49e82e43fb19511d4d5043f53e0787dc4df937ccb904373a", size = 2451701, upload-time = "2026-10-29T16:32:18.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/93/2a6556bbdb8e918175af9d849a5d0b367b5644e41939d040f3a7215b1339/pydantic_core-2.50.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9667637665acba166a7dbbc786f14c50512d3a01f7199e0e1a13b929e92d7c7a", size = 2242782, upload-time = "2026-10-29T16:32:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/90/ed/dd9f29a92257f32eb5b99d5120826186ee0632ecc3aaf6c23e2fab89ed6d/pydantic_core-2.50.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b3e681208e2d39fde13fbea449ef2736a85ec1ea2afa41e9193a8c536e71dafe", size = 2253706, upload-time = "2026-10-29T16:32:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/1ccf3bf38eb4ed97d8aeefc558458834ae0ad53fdbb1edf61af7228ea500/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6eb6ff07964f39e33c6f57fefe939514e76b8c0d4a694e5663e2e04e80cc1798", size = 2288603, upload-time = "2026-10-29T16:32:23.465Z" }, + { url = "https://files.pythonhosted.org/packages/ea/cd/bae2320fc89a65bcd87bf66ec83aac51b3b43b970a2edece15feb8af64dd/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:4eb54c25e3463d73e1e4ed439ffc45d1d7e590df40bc7bb06f11a906afd996c3", size = 2476597, upload-time = "2026-10-29T16:32:24.824Z" }, + { url = "https://files.pythonhosted.org/packages/db/35/1bdb8d52c8e562e95c5e6ebb5784792af568ac6a81470dd546d95f392b60/pydantic_core-2.50.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:91238117cd296352f54f24117ff5a227bdea428865dba1244aa0cdd1424c0bc2", size = 2478664, upload-time = "2026-10-29T16:32:26.197Z" }, + { url = "https://files.pythonhosted.org/packages/12/72/0179b40ac6a6b0899c61826f2e3e80f88063f6d821e0dd9641927de46700/pydantic_core-2.50.2-cp312-cp312-win32.whl", hash = "sha256:98722314e12a8e4fa346e0afd3fff2098b5732875ff45592b450a7b589b43c4f", size = 2108820, upload-time = "2026-10-29T16:32:27.736Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fb/679144362d7e5162519d9f6631ec982521417de3196351c6c56485f5b29f/pydantic_core-2.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:b381574614245f18fdb2d6d1aa8f149e225869c47b5ee7f81812de8977817314", size = 2252238, upload-time = "2026-10-29T16:32:29.572Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/e3ccc885c3e2086ff84d7ca496f45b974a9f0a49ce2da8973aaa3a369a4b/pydantic_core-2.50.2-cp312-cp312-win_arm64.whl", hash = "sha256:f07c965a8aa6d62145f122641f292129984f39a3e3697585b19298a1488287db", size = 2162565, upload-time = "2026-10-29T16:32:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fc/ac3ae9d934932ce3c380e7fe53e01372b49ef5dcb49db56f1a1f005b4d8c/pydantic_core-2.50.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93eb41d626d9a2749a678823c70bba8c25033cef474ae8001fd7621adbbd8437", size = 2222463, upload-time = "2026-10-29T16:32:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/7605223654ed4e777d05668e914d32b476b1fa2bb756bff9820fe270456b/pydantic_core-2.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c539d452cba22f250103c7d1c00cbe9038eb705e657476e701c43374efd9f2e", size = 2093995, upload-time = "2026-10-29T16:32:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/29/4a1a653cfedd3f5e1ccd3b1e919fe324fb45607d12c30fdd7ca5af16ba8d/pydantic_core-2.50.2-cp313-cp313-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:53bd25e8ab54c02bb4c55d4848c1d36f75f3b9a7356e83179fe27794d9e9e6c4", size = 2253178, upload-time = "2026-10-29T16:32:34.77Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/03ce5ed01c98b60272cdee8ac1abb819b64ef347717c11d96215e32d9952/pydantic_core-2.50.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f142cd663d1148ed32993ed9a58e5c67ea312f446a01a6c13a956945aafc9f23", size = 2113894, upload-time = "2026-10-29T16:32:39.241Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bc/f7644474ac0faad9a7fdf5179239893b21339169b8b4f89e3075c4be16b2/pydantic_core-2.50.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:126f158474346f457c0759c29c1a6334acd5f7f99dd272bb27d19314f3d81b4d", size = 2198248, upload-time = "2026-10-29T16:32:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8f/3d3a1c1d313e02774f6c034eb1910eb6e795922e66e81f0f053f98afe866/pydantic_core-2.50.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0b94a515f44f1ebcb25395bcdba506c4cdaf6d3fcb0466b9658521b41e5ff057", size = 2368505, upload-time = "2026-10-29T16:32:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/03/33/38fb4545c00ce2bfebc2a0ffd99f7f580f1e1efb8db3b2faf88def640435/pydantic_core-2.50.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a55c1163e829a08707b4a92c8ee02a5a3a3847ced74aa4da94e3e975ffe1ec63", size = 2451704, upload-time = "2026-10-29T16:32:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/79/be/b559a1ceaff8b1d4581733d47d2ffa785635ac05716eae4c964466aac6e8/pydantic_core-2.50.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31a1fb3ffbd1195b24490f671d12623dd4c9bfa56d6af0a299fa43b516134f5b", size = 2241760, upload-time = "2026-10-29T16:32:46.382Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7c/3c8ab7cb45e4dc7687a4f16a953fee14d2eca18dd90d6be8bd470ef06962/pydantic_core-2.50.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:83d6f42ae6eba44548a1a3d4f900a5b1500fdd14f09f04754a8800ab0d544432", size = 2254115, upload-time = "2026-10-29T16:32:47.818Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ad/3f3471206224cdac2af3492a3f7c724c47a82f5f8850ff364c2e36023497/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:9fcde11462e0faab4c2a2ae8cd4516f7dd8239eec583bd88c5524d8064ad2d0c", size = 2288598, upload-time = "2026-10-29T16:32:49.404Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/2070b388b1d8633cfb72e3964ac3c95aaeb195c240209894ff386847b494/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:848984e231190ae49bc8b5229e64d2a6fba4231dc6046e1b6b28ddc060bc7601", size = 2475116, upload-time = "2026-10-29T16:32:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/96/e96ff19527c73b9aaddce2a4424006dc15439fd89ef2d39641048c5ee85a/pydantic_core-2.50.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8772c2c0091e6fc2d0499151793d31bf811826986b0727052935a95104845544", size = 2478926, upload-time = "2026-10-29T16:32:52.856Z" }, + { url = "https://files.pythonhosted.org/packages/35/10/2fefc082eb46f959e80735e859161305c0c2fbb3d5d20ac5547c49a84a3b/pydantic_core-2.50.2-cp313-cp313-win32.whl", hash = "sha256:bd6ecae08c0d871cf93b6f0d4ee76b06c65be44ea22311981a022b2f1948cf9d", size = 2108354, upload-time = "2026-10-29T16:32:54.276Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/14eeec394ec1422f9c4e9743cd746f4502232a7c65eb235d1a03729d94ab/pydantic_core-2.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:e5c801bdf3cc92274b8fc2c7023c8751e5ae81260c009ac558ff55ce712a2d45", size = 2250556, upload-time = "2026-10-29T16:32:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/75/b7/a014acf1ae4b4aa7d32ff957e2250698637daa3a29448444180360b92284/pydantic_core-2.50.2-cp313-cp313-win_arm64.whl", hash = "sha256:fd9aa71a800c87c0f7ab53fa38009169a23a82c685989626ca74a4bd483e1b67", size = 2162633, upload-time = "2026-10-29T16:32:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/64/fc525bb981a9f287cfa34346e7ed1d5ef7c043c2b9b517f54c6136032d10/pydantic_core-2.50.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:efad0bdc67d3b119c3bab6b94d341a27333c2bb89ed75dfa182a4a06cd0ea329", size = 2226414, upload-time = "2026-10-29T16:32:58.559Z" }, + { url = "https://files.pythonhosted.org/packages/db/68/315701095f25687938ab79d263a6d939337456c2840fe69038fd38000338/pydantic_core-2.50.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ffb4dce3ecb2cd563aa30f9eb5088a1cf1f4f688b90cbb374fa96e49fc50b02", size = 2095041, upload-time = "2026-10-29T16:32:59.873Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/efd3abf31aa24bc97b311735f418886f947b4f193df02a1977643963b03e/pydantic_core-2.50.2-cp314-cp314-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:150f6e101f575c21a4ecfed1be1c05bb89a0ad49cb9c846ed377583bd71b32c2", size = 2257635, upload-time = "2026-10-29T16:33:01.096Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6c/146b36eed4de8115a0fda76840703e75f5f8b36aeb3af86976bf094d1a17/pydantic_core-2.50.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1db9cab6197e797824f0479dd2ae5fd2e272aa61c61b7faa5a6e450ca7a0d3b7", size = 2116807, upload-time = "2026-10-29T16:33:02.692Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/db006ea79bbe405cc38b25899319c7d461ff1813cbdfe6aa90a65ed1dfff/pydantic_core-2.50.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1b9f652e17a13bbc09b45f74c3c326cca72059ce4dcdef687fcb04389db413a9", size = 2201037, upload-time = "2026-10-29T16:33:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/41/f9/ea2bc1ec13a67af4a63c9a887fa0aa6ef4ade9018e2bb412d4fc80110e19/pydantic_core-2.50.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:823b222c4fc861a82c650998a6117ecbd150cc71bf8b78f9a31bf3f48c456108", size = 2374904, upload-time = "2026-10-29T16:33:05.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/8e/26a6a075301f0e563646037ea69bef8c63911d38fbd257f94cec69cdf985/pydantic_core-2.50.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:783abdfac21247d33bec40aa683d26bba30d61655c40de38b2decc4797aabf92", size = 2457626, upload-time = "2026-10-29T16:33:07.271Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/8eeabaf1d394f0d3b9a27129ac21556eabee6f2ddcb418069ef4037e00b0/pydantic_core-2.50.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b29b7c5adf4b2be7c941f7f76432aa3269cd5052ff8610376b8ece24d7b02c51", size = 2245505, upload-time = "2026-10-29T16:33:08.824Z" }, + { url = "https://files.pythonhosted.org/packages/bd/54/449778cd397e2682b30d1bcfab0872f6ac36702b59cc326296b7f4f14928/pydantic_core-2.50.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4ac996e42a93d80122a23edce44e02c6b9c4913f5c6198a077da31475341a39a", size = 2259563, upload-time = "2026-10-29T16:33:10.107Z" }, + { url = "https://files.pythonhosted.org/packages/26/c2/2f65691d25488ee0f3c17861d079e8ccb7817955522d07cd3c3970d30af5/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:99e1d482138a71902415ba375bb4627679a5fefd3bd07803c3a8f37632f4981d", size = 2290470, upload-time = "2026-10-29T16:33:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/aa7c40234cd24536afa0895ff1d26f9f15638004e25242c5193ce053b96b/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:a53d5f11cd6d15569ba3e53f3498077f0d5e633f6ec604d2ea613e895017ebee", size = 2477272, upload-time = "2026-10-29T16:33:12.771Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/c347c68e040cf93d8487a8f4fc28f2768312834c7d21c301538f416f9955/pydantic_core-2.50.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23385c4626f8b5a7792aa381ea573d9432cfbf4c21375a0a5eed2a4fb64e0e58", size = 2484352, upload-time = "2026-10-29T16:33:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/72556126c8b6154fe7acaf757b96b2daa37aeffc3a517deb76e272f5ad09/pydantic_core-2.50.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9a8a62281fd9c6a373b3fd3d473685e6838d43341eecde5f47729748e2c11ee3", size = 1491226, upload-time = "2026-10-29T16:33:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/66/004c85511812964a36b3535af022c5c0dfcb760a31e4905b96310e8dd6c8/pydantic_core-2.50.2-cp314-cp314-win32.whl", hash = "sha256:18f008ef58cd30782f81b6e0119e20b7e884edcefbc2d370162ebe033f6e5c99", size = 2113134, upload-time = "2026-10-29T16:33:16.87Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5f/dcd3df20ab2a7279d42a8415fc184e8e77bcf9aa3c9ac3e938c1300a242c/pydantic_core-2.50.2-cp314-cp314-win_amd64.whl", hash = "sha256:b62c89545d21d930c333208fd548e7b4f262228ba3a322fd75c987a75ab11f31", size = 2253407, upload-time = "2026-10-29T16:33:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fb/0685895f5d199b215011dc4eb8ccea032a64ec23dfa88eeb5acca939c2d2/pydantic_core-2.50.2-cp314-cp314-win_arm64.whl", hash = "sha256:6e731bf5a5e8e25398544d7709e39068ec4230b0177e4db7fa955692fd415ba3", size = 2160456, upload-time = "2026-10-29T16:33:19.782Z" }, + { url = "https://files.pythonhosted.org/packages/84/2f/4c8f67efe62837a508b74cf910904f73029cb211371351c5ceb36d56e85e/pydantic_core-2.50.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:33c6800ab9563adaa0d7893e9cd8f5b972ef1e6a23df70498e05c24f3fcec5e1", size = 2217558, upload-time = "2026-10-29T16:33:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/e0ef88cfbcca5409f57751289bc20be63a58af596a36e94b8cd795e59747/pydantic_core-2.50.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f81df206b937d0dca5efb38c25fc99037d82dbcd3740e34c3f81997d2322ac1c", size = 2074335, upload-time = "2026-10-29T16:33:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cc/47844a3179ebce27064e13619594b3fc1a9b4509d8dec1e3960c5f87f5bb/pydantic_core-2.50.2-cp314-cp314t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:21ea3b00f1e87c66f3f9e92f9f6fec20495ac0e60d7d2e68ba1fc49739d7cf40", size = 2264201, upload-time = "2026-10-29T16:33:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/30e9d3c86d81884ee3199560ddfa2c1a66a8dc0736441748eae43064abbc/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eff570402175c7d5fff8497d5a9c05580664a79bb66c07b8f205d17f69e580b", size = 2094353, upload-time = "2026-10-29T16:33:26.739Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/392a5999480d0bcab4539db0dafa93265152396fd63e87be4d24e13d6d82/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:240fec32a74af0979f7253532859b3fd209ad5eea822416bb010929dba445c3d", size = 2203119, upload-time = "2026-10-29T16:33:28.154Z" }, + { url = "https://files.pythonhosted.org/packages/cb/80/6e4e276a3fde5500224a39150e06a4f134f136165908be69402720718a36/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92acf02c770b9d5c4e9a3e8d64e9d95ae26456a86ebb7b7cf64a97f899e5871b", size = 2392881, upload-time = "2026-10-29T16:33:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/d5/5eb56c6317ab4abf33ef9519a50bfac0c96f7f408bad1bc36da6f27d43e4/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1ac5b99bc7b0a68ec64f5f75a6828a7bab5535c8df0cdc07901deb50e0815910", size = 2457290, upload-time = "2026-10-29T16:33:30.587Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f1fe9384bb37e4e728c51c79151808deacf7b2502e245f50b57c85eeb199/pydantic_core-2.50.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:713d7c33c0e231e3786eb0ec95885814dd65d5f3a09754399b6acbf5e9365f11", size = 2241568, upload-time = "2026-10-29T16:33:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f1/cffc4f8456c35690bcb65a8f2304afbf2f519766345a28aba553d5a55cc8/pydantic_core-2.50.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:61e13100b30d8d25fc0d1f0297014b32941bf0975a3584bd08f3adebb68d1ead", size = 2257185, upload-time = "2026-10-29T16:33:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/ccd4df1cc39cb1e42fb7ec86a0dc0a23e8479de04d3012ca6b0bf2279a03/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:523fe6574aea50f456cbc301c1cdddcbf237939f7657f9af99b4f0f71942b59c", size = 2299384, upload-time = "2026-10-29T16:33:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/404b4b0c416a61867ee642cdc1450bca4d8a9a10e5f5f21e1ecaad05fdc8/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:6c25b8826be85714b27dafe908d3461e1b09e3e31c0279a7029040c832c781ef", size = 2479384, upload-time = "2026-10-29T16:33:36.106Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/57c81418a2fdfde0578c71396df773965adebd4421d9985a0f8dcbe9ec4d/pydantic_core-2.50.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a59db03b80a156a6fbeae41096866d1cb89cdf31031ba44c768a804538880636", size = 2504616, upload-time = "2026-10-29T16:33:37.423Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/dec78e28a604f3f4985f5a795d4da3e5fef78b0921aeb7f614ed7c780a32/pydantic_core-2.50.2-cp314-cp314t-win32.whl", hash = "sha256:0d090ae76d5b09c003600c7779b3adcf3d1b49268f616c5c51d844e54c8e318d", size = 2093179, upload-time = "2026-10-29T16:33:38.705Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ea/79d9ee60648463804145d44b08f1e7e2474427d9704e3331d814483908b3/pydantic_core-2.50.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9b01a092955c1c7530196ad526755cff4f2d11f37ca9a6fc9ebe37ca49a48977", size = 2220575, upload-time = "2026-10-29T16:33:39.929Z" }, + { url = "https://files.pythonhosted.org/packages/eb/01/aa6a7bd017c3228a41942da4fd788b86fb4aad9677212a4737a0c157fe74/pydantic_core-2.50.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4ea01f30e119f1ec7a2034e85c40f491d469896aef02cd6436fd9292fe1be3fb", size = 2160866, upload-time = "2026-10-29T16:33:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7f/ec2974015b4be5e50266ce732b8942d3d0b3bff751ace8900999c6577bce/pydantic_core-2.50.2-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:a3317630e2791f56071b82f89bb71624146125e4311e6643f708c6ba744b5ac5", size = 2230936, upload-time = "2026-10-29T16:33:43.656Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/af99a09eddc348c1d0744c0df28dfcd4839ca497067f0fa2168208b41b1b/pydantic_core-2.50.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f73930dacad5e611fe019e18df6f53c30629b1843d038d303b8dec86a32493bd", size = 2100409, upload-time = "2026-10-29T16:33:44.946Z" }, + { url = "https://files.pythonhosted.org/packages/84/75/8db3c46f02d6005277e5c2ff5933c85e7e1eb8798aa334c676c828650c3f/pydantic_core-2.50.2-cp315-cp315-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:e2e6240a0a5ad847017b3d65187877fdb7a11d5a58d43c30736f9f523478913b", size = 2259728, upload-time = "2026-10-29T16:33:46.461Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9d/98c8d524921ca24e5c8a81a4b8f12209ac6ab05d0dd58b374e357da0ffc5/pydantic_core-2.50.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fdf5b52ed22f1404c2433d0b46df2d86d9befb742d0b9b4042a2325c43cdb97", size = 2121771, upload-time = "2026-10-29T16:33:47.952Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/68c3b1fa919946012c940dea88e94e33e7dde46abe3e3d57053793c9bab8/pydantic_core-2.50.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4e9392ddf67fe97ab9889f796d468c9908a8754b8b6c7520eb03a65012b9a1e7", size = 2196328, upload-time = "2026-10-29T16:33:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/cfb6312a5b65b02eab6bc00fb0d258d0e2e49468dab29b430e7cb49b2ceb/pydantic_core-2.50.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:48e944091b7bff233920bed8a60b0ed6c96c73d3c5836bf51c4b9a4bfb58e4ca", size = 2380152, upload-time = "2026-10-29T16:33:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/7fcced1d84ea294200abfa93e488f36d00732101d17bf5e1f1389e66de26/pydantic_core-2.50.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:91c92856b846aa690ea6239ffd6c5003c5e3e296a69095570bf4efee5bfa0bdc", size = 2462299, upload-time = "2026-10-29T16:33:51.884Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d0/dd0e355694094f0045922f4da60ac9bc7a314babc494e46089fb00640fd3/pydantic_core-2.50.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5299296ff5cafcb69851fd652de8f9268a0bb036bcbedbfcaa94cf743baf3986", size = 2249999, upload-time = "2026-10-29T16:33:53.333Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/f5f4b36434604d22aacb7152667f87a470e68847c89b136c68a278cf409d/pydantic_core-2.50.2-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d3d049b557875f9d15230eec8a731749a7a873d8ed6a051903b05bdf46e0e65f", size = 2262745, upload-time = "2026-10-29T16:33:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/a4c47a286745857758a4bb8d4d5e260c736a7bd6fab5da618f5711d1a3f1/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b0b25d647036972c2951059046cb498f72b7af5a2def27bf8e5f44c3067ab7dd", size = 2293663, upload-time = "2026-10-29T16:33:56.132Z" }, + { url = "https://files.pythonhosted.org/packages/0d/db/25330e609fd219ce16d0a089af1b4b62095f01595dbe6972f7a7dfc313b9/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:8835566b3e94026da3ce2249f9ee35c9d2ebe4cd131f6a39bfc82708d3010a46", size = 2473247, upload-time = "2026-10-29T16:33:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/53/16/704ca88b82156dc485da2dfb11d5d52a10b9ef15a294639c0aa5ebd1643e/pydantic_core-2.50.2-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:3fbcd402e4b607d4b8de5d4591215f31f042af03f9d09e58f693a6221e32833f", size = 2490536, upload-time = "2026-10-29T16:33:59.297Z" }, + { url = "https://files.pythonhosted.org/packages/50/34/1fdd0eebee9f8b6c4ec3c2490f49dc59a652c8c5a7fa0cbcaada0d91cda9/pydantic_core-2.50.2-cp315-cp315-win32.whl", hash = "sha256:8431ecb131489cbdaf6f51f7ade47475f73aff4f1912552f75ba19a6cfe233a8", size = 2117204, upload-time = "2026-10-29T16:34:00.514Z" }, + { url = "https://files.pythonhosted.org/packages/24/6b/702adc91486fb96ba55675d58ee15ddc38fb48553fb35e26fd005e303206/pydantic_core-2.50.2-cp315-cp315-win_amd64.whl", hash = "sha256:04cf672960b8c4fc28bb159c8cdd101fbd311c520237a5c0360448b851adfb41", size = 2261064, upload-time = "2026-10-29T16:34:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/7f117432b8614526aa0d96fadd2b7fb7393156ddf2caf26a6fe606e6cfb6/pydantic_core-2.50.2-cp315-cp315-win_arm64.whl", hash = "sha256:36917e29c0b815d6b940bc91560fcf453b5b85d55c579796373f0ea11d1c111a", size = 2161783, upload-time = "2026-10-29T16:34:02.966Z" }, + { url = "https://files.pythonhosted.org/packages/45/1c/2ba7a34fe8da4c83ec200fb9f85ab4d54b961be08367922743623f987849/pydantic_core-2.50.2-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:bd1fe69056a19c377a6c6de2c1240fb8676ba0512a969965efb486510c5e0f1f", size = 2223184, upload-time = "2026-10-29T16:34:04.214Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/f71a8da1f53a5be1787d2a97ba6fd9adba115305c7f15ea0bf733ed8336c/pydantic_core-2.50.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9b2383933b1dd7a8c93e02f99ebfd305f1286057172483b8e6898bed3fef69d3", size = 2078174, upload-time = "2026-10-29T16:34:05.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0a/847e1f5427372692f6eb5c38261fed6b8cedf379e37d9f6134d48b9c8ee9/pydantic_core-2.50.2-cp315-cp315t-manylinux1_i686.manylinux_2_5_i686.whl", hash = "sha256:a1b4d60bad57d6e0e7cd30cc4d1dc646d258dd6dfb7935ce76c7dd72dd1e98b0", size = 2268813, upload-time = "2026-10-29T16:34:06.733Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bb/12966dc82b5e9c347981658b52e24349b6242902b644725c00ee7bdf4267/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:93bedc68c4138088ea3c042189c01d2945f530f0dcc36e1ea02df8418bd4f1cd", size = 2098331, upload-time = "2026-10-29T16:34:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/340416b7ebe664524e7ada16ad2e3e069888b168010d75bda6a502bf2bf8/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:d684ff87d00de2f6abcbbfa9b0fdce8b513dd799a900809dcfa7eb62ad69832a", size = 2197432, upload-time = "2026-10-29T16:34:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4d/425162c3d88af0d190c4dc4f940402cc6dd44d934359a37f50da6b7e4672/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb6d49f00e2babd40af4889d6b9f4bf50879f522dd4e2e22cb705cac5e0c5e8e", size = 2399300, upload-time = "2026-10-29T16:34:11.462Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fe/2882bddf2e2038c8f41fa69f16ddeb6eeb1c002cc0cc0eb2157f73bb34e9/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d47b2d0e00e030e0ebdb08115a37da7294e7a6d196d134a77ed7ee6c142284b", size = 2462672, upload-time = "2026-10-29T16:34:12.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ac/d7e6bfc6a95573647d1a699e19228dd99914ae6aab79a2bec293a038e980/pydantic_core-2.50.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95838be125133fa314753f8cc5168f7c41e3a9dd5eef5383dbbbeed511bd4853", size = 2246293, upload-time = "2026-10-29T16:34:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/64/97473ccaeb4ae5eb338162bd8479af121484e67f7078499730e3ee0a5592/pydantic_core-2.50.2-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2d4a043c820363359e40ac96270e1915e5376e116be1f59cee0701775de3e376", size = 2259507, upload-time = "2026-10-29T16:34:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/78a9b7a6a0d0679ce1e46cdd38ff95afd18dd2d6a01ed87c80a01bc003f7/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:57bccd3e1128736c792094da1feac6da46ec3feb1957ef649a9b3e4f8de6fa04", size = 2303550, upload-time = "2026-10-29T16:34:17.162Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/b697173f82e99cdcf5ec6654b2edb66a00f2f92ef9d9a168861fca3fcded/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:24120c644322a35b5eb765416ac8442214c45689e255f5fc6e761e7e7c743c8b", size = 2474919, upload-time = "2026-10-29T16:34:18.841Z" }, + { url = "https://files.pythonhosted.org/packages/71/10/986be76723784ae87f9664658fae39df340eaa21681cea95b6339d2e1b3e/pydantic_core-2.50.2-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:b1adf2f37521b578c438f6ec61d1c1d0c20b076863ddd5d8b6f3ab658857db3f", size = 2509095, upload-time = "2026-10-29T16:34:20.16Z" }, + { url = "https://files.pythonhosted.org/packages/5f/0b/38dbd38c67de7642f87348da70122b89ca4c5fe8227cfdd5cc00c1cdb454/pydantic_core-2.50.2-cp315-cp315t-win32.whl", hash = "sha256:d79aaa468193c1bed6b0560d3c5c9f9e369e3efb0b128b38a2537e2d51d42773", size = 2096972, upload-time = "2026-10-29T16:34:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/154baf8413b08d6daf2f5dec211c303ec6bed50c032717f354f8dd85c994/pydantic_core-2.50.2-cp315-cp315t-win_amd64.whl", hash = "sha256:cc488daac19e6f5292180fac5e0ef7efffabeef4e096af9a17a75a7d3d6781f6", size = 2226659, upload-time = "2026-10-29T16:34:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/eea7a1e7d98a79ef11b6d31748b70c58f8723f87d7ee42edbb6a101f6245/pydantic_core-2.50.2-cp315-cp315t-win_arm64.whl", hash = "sha256:91315aea7adb51821d4a3378ede953bfdd76e3bf71551de4a82d7638cef7dc02", size = 2166387, upload-time = "2026-10-29T16:34:24.433Z" }, + { url = "https://files.pythonhosted.org/packages/64/4d/23560b11366b547e3e68a3b8f9c02557cee707effa54fd39dbb3c4fff1fa/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:d18d56c1ec63b304f4bb3c7db7e0d311e61cca1b1c944590a4b13ac39b2ca92b", size = 2204868, upload-time = "2026-10-29T16:34:34.745Z" }, + { url = "https://files.pythonhosted.org/packages/06/58/b9ddfaad189143d6568627166a730b6624ca90bd9a9a4af4854df88d444b/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:41ed8abeffda6a880207fdc0cd1343a448cec3cc6b38b53f730c0745f745292d", size = 2049162, upload-time = "2026-10-29T16:34:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/8b/20/99d259bf7835b2f13c6d0b3eb7bcf669fb6349986736fa7a8308a9e620b0/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b7abcfaf66c1dc14c5ed928c349614f223c972a8b021b4be9548dbea6c817371", size = 2112135, upload-time = "2026-10-29T16:34:37.777Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/652c8fe599abacafd852479059d209e608596ad3de82f1bff22e9f0f02fe/pydantic_core-2.50.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7460abce637f90e37f8c443559fbd687c38884d05cb69ef265e634945f4324", size = 2270339, upload-time = "2026-10-29T16:34:39.039Z" }, ] [[package]] @@ -274,7 +274,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.1" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -283,59 +283,59 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, ] [[package]] name = "ruff" -version = "0.17.2" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/58/690cf56da80c8f2c8bf12042b6017beac2c34fe961f65420bcb3fb7fac38/ruff-0.17.2.tar.gz", hash = "sha256:84d632402d14c23b488f941db23712d6736a013225162062d34558c3f3c780eb", size = 5333400, upload-time = "2026-10-22T18:38:00.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/b8/8b6d69c57e8fe1cc6af1aa7493b72a1a5b5a8afe34694e302025441729d4/ruff-0.17.2-py3-none-linux_armv6l.whl", hash = "sha256:96a65e755d8dba094707fb23ab602111d9a31ec2835b3a6992a1914a8fcb211e", size = 10183857, upload-time = "2026-10-22T18:37:14.956Z" }, - { url = "https://files.pythonhosted.org/packages/e8/61/12cc2ff7b2fa90a268ce1e2162c23aaf76e601d22623f6a31b0eeaa764b0/ruff-0.17.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:39c5b55c960515cdb32be16e69efb5734c589c06a0eb38a22694524a336b889d", size = 10210904, upload-time = "2026-10-22T18:37:17.985Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7c/b8850a8e4ad29f64a827dd6c6902db4a9aa538fb2b9eef4e26084001a3bb/ruff-0.17.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:92a80d2f44584dabb0189b3a6c393e37e525e04942838482010575cb81945be7", size = 9408439, upload-time = "2026-10-22T18:37:20.964Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b6/6a64fae86ca5e5e29d5adc311e5481de726cedf9aee607435a16c31c442a/ruff-0.17.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7d0906677d89bbf3cf73cde64b5bbe2ab35df3ae43e49ee2627301e91265bac", size = 10047078, upload-time = "2026-10-22T18:37:23.717Z" }, - { url = "https://files.pythonhosted.org/packages/10/19/23d376547715175aa1bd46df48df6aacc7c6439d9daf19cac15975180aa5/ruff-0.17.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cfd783590edf2b77317882631900f6a13ea2e8eacefe6ebf2370d2715299160", size = 9990478, upload-time = "2026-10-22T18:37:26.584Z" }, - { url = "https://files.pythonhosted.org/packages/93/6c/34cda32cbe13adac5c6b291a307a5570d8ebd37c0c86339f0fe628e55632/ruff-0.17.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122258f6ecde0e0dd1bb60b24263c3edde4338caf723fca711cc729f9bfd414a", size = 10673624, upload-time = "2026-10-22T18:37:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/bf/10/5d762a4dbc851d8bb47863cf3a28a61bf31c804d393b70d891fac899f5d1/ruff-0.17.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f8fba429d6ac4128ab29cfb8fa6240d2fa671d04adf7f84d3b22fc7b6b6bea4", size = 11351797, upload-time = "2026-10-22T18:37:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/3f/04/a0e92a81faf5c81c1c98d4374c8401734ccf2b8cca7d0170cb0fec08c6e2/ruff-0.17.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:583bd67bb9e7061dd09bc46a0a884bae2bbd214dbcd48e509813612619683869", size = 10949414, upload-time = "2026-10-22T18:37:35.501Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d8/59894cf630d2d86882457a2a6a0a1533410c5c805bfbac6f6eb4b5aef69d/ruff-0.17.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60f93fec7142dba5b8f487f94bf2f6231dae24dc11e4c48648347c8b0720004", size = 10528000, upload-time = "2026-10-22T18:37:37.88Z" }, - { url = "https://files.pythonhosted.org/packages/ab/95/d6e30c51e851d37e10762ad91fa1e76ddaad998589b5667962a3cfbc9edd/ruff-0.17.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c555a877f8f41447e318101cfefea9bf12dfa9f82eedf90c376e57e069f917d0", size = 10571779, upload-time = "2026-10-22T18:37:40.444Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5a/a7e1d74fb464250137d3d4eca645d323536533b672004d04ee0e46e0caf3/ruff-0.17.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ba405cbd5ec7bda1f555d5e52be1fd68d02608d4d95d24591bdac92cfacb579f", size = 10051707, upload-time = "2026-10-22T18:37:42.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f2/02fca29ca7999380f0350a7212fe755c11ab9c22d61327356cce8decdbc6/ruff-0.17.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8c21b4ab24485358f17291a4f8dc5399b834e963b8efedd920fe4fdb02081bb4", size = 9998227, upload-time = "2026-10-22T18:37:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/00/55aa604a093dfdc1526e570fae0bb7d37a2221c7b4728d5f080b2add16ba/ruff-0.17.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5875a16b0de70b43cb81eabea5ae5e8f5d1b2fd1b5e6ed07f80cfe5019d0bd5a", size = 10377692, upload-time = "2026-10-22T18:37:47.704Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5a/a7a640f47bb906ec8fa233ea84319d503e6dee02b59e3901bd312ebd1f9b/ruff-0.17.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2d086be452eed686aa51271ecf2cbe5037956517cc6ee39a17b4489ac12f8bbe", size = 10790587, upload-time = "2026-10-22T18:37:50.191Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d4/c3b74e1b4808438ea7f14bb914478025e2d51261f60774a54a0fa222ec46/ruff-0.17.2-py3-none-win32.whl", hash = "sha256:6bce7d1f5165701ae18f5df85c65afe78cddfa6c5fb4b9bdb0014d60388f3edd", size = 9870537, upload-time = "2026-10-22T18:37:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/c7da96c2c96d20704cf9027f7a5121da272a8c05f1d1531ba949e3b3df04/ruff-0.17.2-py3-none-win_amd64.whl", hash = "sha256:1b59d447f197bef9ef48a22ba1a6016137753c6099e9bac9c36eb566e422424c", size = 10282826, upload-time = "2026-10-22T18:37:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/81/60/a3604c79cb02777b6cb84da894104021986f06c4bc41375102cc95049a32/ruff-0.17.2-py3-none-win_arm64.whl", hash = "sha256:195ad918027f817c119310ca89a42d347887dfc28934aaa869325e9c5dc9d962", size = 9827036, upload-time = "2026-10-22T18:37:58.031Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, ] [[package]] name = "ty" -version = "0.0.91" +version = "0.0.95" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/c0/9e26817a5c94d504aab30bffbbf53f836f4763dd0a947089246ff555eebe/ty-0.0.91.tar.gz", hash = "sha256:b9c434cc1e3b106b6b9353b78ef304d5904c82cf440abcafef7134270a22b41a", size = 8035848, upload-time = "2026-10-22T18:24:52.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/08/d63954788578b9189dbf85081d8cbdba9a32a9a6a29729129a0da09cd3a0/ty-0.0.95.tar.gz", hash = "sha256:a480853a74d43074b2d4b28025d406b9d7223f7fe9f3a6c2adec3eacdbf14ca0", size = 8115525, upload-time = "2026-10-29T19:21:51.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/ad/0a850c1ca8e6e802e6b0f730f0d59c9769845b2fc956e3acd7f429b665f2/ty-0.0.91-py3-none-linux_armv6l.whl", hash = "sha256:5d39e6e485854838eb7fcdb747495f5b90283d58bce869c17445120fe0c7e40a", size = 14255787, upload-time = "2026-10-22T18:24:11.284Z" }, - { url = "https://files.pythonhosted.org/packages/33/87/9ccfa2d97b8b043e4d6fab10febc50ac7856b02cf7cd64910085db806bbe/ty-0.0.91-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcf953e25f90fbccf6cc27ef43fc4ef0961c89f54866e32cd1d7f24891b96084", size = 13822267, upload-time = "2026-10-22T18:24:14.31Z" }, - { url = "https://files.pythonhosted.org/packages/13/33/be8f62444c7840d1b0928c6d174540c0fe6b1b193ae9324c8249e8ed52b9/ty-0.0.91-py3-none-macosx_11_0_arm64.whl", hash = "sha256:78e3e66e88a9ae903afe0981ff9bd225d452ade2aeb070715842510dfcbf6de4", size = 12957620, upload-time = "2026-10-22T18:24:16.961Z" }, - { url = "https://files.pythonhosted.org/packages/31/9f/a742f273c065c4f5a054cc2b44076207136ff1cd346adfb23da38d8894d8/ty-0.0.91-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ecaf87b3d7b508f9ea1ef8715d4cf47942719659028c92fad819d82a82ccfa", size = 13746018, upload-time = "2026-10-22T18:24:19.221Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a0/3eecf8717cbe4e05d6e02b53679aebad79f1bfcff6c9c03d7601cb7df5f9/ty-0.0.91-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ec8fe4243aab445abece38a2b8e9b3010230d5e6b3f4e15bfa811a914d99e73", size = 13993220, upload-time = "2026-10-22T18:24:21.328Z" }, - { url = "https://files.pythonhosted.org/packages/09/b0/fb3edee2857e97d85b6c735901469fd523cf6792382e4fbbb00b065c186c/ty-0.0.91-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:577701ec2a9cf7a50f6059069399b9437bb8af7a99df24012776e95d8a55831f", size = 14936911, upload-time = "2026-10-22T18:24:24.393Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2c/82f29ee86f4171807af8ab2feaf4e51211143118a3e53e9da2c2e77eca7d/ty-0.0.91-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc683f49197d63ec47b2eaa9763ddb32dafb2a31190d938b9bb8ebecc5b25173", size = 15336384, upload-time = "2026-10-22T18:24:26.472Z" }, - { url = "https://files.pythonhosted.org/packages/0b/85/e2419b19577ead84b8afa9137c12eb4bde19f926423bfe01e615db12c52c/ty-0.0.91-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8bb2784572fa22f77e417844936f6a9f831a6b0ba64749ca1065c407aefcd23", size = 15332319, upload-time = "2026-10-22T18:24:28.979Z" }, - { url = "https://files.pythonhosted.org/packages/83/06/dc28cea0796ee0268bf987d1d6f0f168605bec8898dbdbc7676049d2667b/ty-0.0.91-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68bb6bfc8171910335a47c7e84070f28bbbc0326bcd6ef08fe3690d0509556b", size = 14600250, upload-time = "2026-10-22T18:24:31.359Z" }, - { url = "https://files.pythonhosted.org/packages/1e/71/83dd28e628698059ba6fe7081acafa7a8f65cfd90fe6573907ca8219d561/ty-0.0.91-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:63f268c80697d614d18e988476d2517177d66ec732ae104d86e57b60f197a416", size = 14812339, upload-time = "2026-10-22T18:24:33.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5a/63c0eae2d5b02cef98a9b327b1a16b36287c140b3a7d2306ff7560177f9c/ty-0.0.91-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b0f34630aa92e222d7754d66222d116f027025cb2b9ed66e1b4b73623de74e84", size = 13703060, upload-time = "2026-10-22T18:24:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/fd6d20d00202949d775043c5f70679f38d0e48ea30eeacbffaf6e0a9754e/ty-0.0.91-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e62f8bbc17d481e01cce39bdfbe36beefda0478cb210e8045ef618d3f674a731", size = 14006236, upload-time = "2026-10-22T18:24:38.464Z" }, - { url = "https://files.pythonhosted.org/packages/fa/26/53dddd4f927dfaa73ea14cd71d97b4d92d685e365b5b037bac457e271403/ty-0.0.91-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f8bf4514a383b13d7cf86eddb55f0eb9f356fc7efeae0de714ad4fe5fc54fee8", size = 14356612, upload-time = "2026-10-22T18:24:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/89/40/ccebba145dd244b2f6058ce2e4a6ff34dea36d4a5b38dd7a6ae983b65a3b/ty-0.0.91-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a6d93ab988c4c5ed92b52389b888d46ddede78e4646fe28db615bbeff89cdf82", size = 14674843, upload-time = "2026-10-22T18:24:43.325Z" }, - { url = "https://files.pythonhosted.org/packages/08/6f/8a7701adf1f539a893274358f12ba62773d5bb40920bfc53e0d852bfd576/ty-0.0.91-py3-none-win32.whl", hash = "sha256:b9f4bba8486a3d13babc11f8115369d0a0dc8bef127b7cf4f4c5eed9d4dbfa01", size = 13260545, upload-time = "2026-10-22T18:24:45.715Z" }, - { url = "https://files.pythonhosted.org/packages/fc/63/27c8051a72328ee81f47dace5e2c63e7db43bcae935db25a9dbd4d0e59b2/ty-0.0.91-py3-none-win_amd64.whl", hash = "sha256:5ac9eaf938e7fba4dd6c3358f852e6eb3127aa7bde0a76d9807c41f60ad73a29", size = 13759484, upload-time = "2026-10-22T18:24:48.111Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a9/1f6c3694b7b33be4ae9a272f71a99a7dd27ec020c32a22049e9c0e79f576/ty-0.0.91-py3-none-win_arm64.whl", hash = "sha256:35820957639db1fb8c643273670967192c308038836005bdf3bfef111aff64a0", size = 13318343, upload-time = "2026-10-22T18:24:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/8f912c9d91503aa26b704838e2ecba448fae5713a799e2022e03e20d5186/ty-0.0.95-py3-none-linux_armv6l.whl", hash = "sha256:4e0745137dbd472027e71827ef9dec803439395319a182835fea2bb7ccc264cb", size = 14526217, upload-time = "2026-10-29T19:21:11.573Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/25feb9b4c04da26d2aa91527b0cc3de281aa45c9857abb900f1c4275e1aa/ty-0.0.95-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b9587b262312f94bce6b8c139db0542c44a1dce8c2224b4d33ab649b287b119", size = 14099685, upload-time = "2026-10-29T19:21:14.433Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6b/2a084a72c8069f8496fcc0f576ec053f648b1baeed07532b3eb84c4982e4/ty-0.0.95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e7902477410e897a60d2cf29ca89c28ef2a2abdb81698d8ae4be2e33b934d4b", size = 13144133, upload-time = "2026-10-29T19:21:16.734Z" }, + { url = "https://files.pythonhosted.org/packages/84/ad/e4088e627b3e3cd8cbb6ad08668cde33fc7093887606de6f23e4e3e639b9/ty-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cef0d06e2f286db6596e004f5820d4dd3b3a1a698465840e89a5e8a5934aca7c", size = 13996368, upload-time = "2026-10-29T19:21:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/67b0ee993698496e0c93b5afbec7838cc26003ac74486fc11d89c15b52fb/ty-0.0.95-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e032018e2aaeba51a48b639acacfd949be335cd2395faa205a305a9ff524b9f", size = 14262316, upload-time = "2026-10-29T19:21:21.303Z" }, + { url = "https://files.pythonhosted.org/packages/14/23/7214c42a9d8533c5f8a6384ba6d82adda9adab2347d5fe1c395c32c4e4e9/ty-0.0.95-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c98a2ea2d1a731d0db27a440bceea055ba3555e3e80d694e567110ba2a477a", size = 15217155, upload-time = "2026-10-29T19:21:23.597Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/4f51df84ba70438fbf2d481baa59cc8801d86e26a472a1c0b885c01c9c99/ty-0.0.95-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c26f09e9d6039f5ffc1b35e24830da67ff6202fef8583991e2ed169c0b188a54", size = 15607522, upload-time = "2026-10-29T19:21:26.245Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7d/958cfc1e9cd56390f916f36d97a00b7aa15db88b2cf1a39c22f524c6078d/ty-0.0.95-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e01e493e87aede663405e6f2cf38c65a95456c8181ab04e9e2e9662538ea717", size = 15562445, upload-time = "2026-10-29T19:21:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/90/64/636308717989afa2fe765604ae2a76189f470d566e5e58da4482b625d64e/ty-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d47be2db5db80cd70c582b240a71dd8e9be215ce9288298c863c7c3611ef02b", size = 16312046, upload-time = "2026-10-29T19:21:30.801Z" }, + { url = "https://files.pythonhosted.org/packages/a9/75/e57a2c0bdce3dcf3d43ac15f3c9adc34212330e056db8a70208ab7f3730f/ty-0.0.95-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3843233ed7ddca6d1d2e38034bf28d2618ff2fc8b16190d2efbbe46b066ea524", size = 15070529, upload-time = "2026-10-29T19:21:33.7Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/2efd45b625fef61491986feae6f348449051ee474de4d79a77819c678aa9/ty-0.0.95-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0b24691b55fc64d480d612bbf555ff369c80d56e26c33af94acb2cbadf1a37f6", size = 13962242, upload-time = "2026-10-29T19:21:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/d5082fbb65305af5689f1e50f3b6b8897287f1ded5d06ef713faaed17e97/ty-0.0.95-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf72de3cc1d528b1b19020209f4692997f402bba40d8b3e5654906edf61d1c84", size = 14265678, upload-time = "2026-10-29T19:21:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/2347c9519c35179d7e2ca74b0ea0b9d78a503d6673905ae911dfcc13f2ed/ty-0.0.95-py3-none-musllinux_1_2_i686.whl", hash = "sha256:721260b0dc05f9bb3fe79db642a2eed94aec99de2d995c7c92e56ffd8637b1a1", size = 14636684, upload-time = "2026-10-29T19:21:40.466Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/bac1a90c887c0251955ed17e5da8139b4ac87352d67fcb3d1ffdb1b8723e/ty-0.0.95-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:156abe019cea9aa54e581d89ca34a48f81b8e4e9c49c203943feb60a06b822af", size = 14985737, upload-time = "2026-10-29T19:21:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/68/ef/8840c75892f50107a541e7ca3fc0912d9dce133e4e13728838981901e1c1/ty-0.0.95-py3-none-win32.whl", hash = "sha256:b1903d8c17645c29ee566e708bee569d971c461f3429872305a574ccdae4c732", size = 13503067, upload-time = "2026-10-29T19:21:45.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/df/46fdcc84de81159e1d40ca103d24ac08b2450a2a51a1572333e8e796b8fc/ty-0.0.95-py3-none-win_amd64.whl", hash = "sha256:d07bd09f8a525759a595e04051e923aa54f8dc483f90e6a0b003d77a3fae0f04", size = 14097073, upload-time = "2026-10-29T19:21:47.462Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e4/84cf4f3c313448d97550fdce50e7c70b934b38765772dd4f7fdaac064cca/ty-0.0.95-py3-none-win_arm64.whl", hash = "sha256:74ac66df3cb1851e3f157c4e664aec1f28857b467c7e13f62ccf53efc6884c82", size = 13575281, upload-time = "2026-10-29T19:21:49.773Z" }, ] [[package]] From f8bfce3393da131243c3e3daebad015ec18cf42f Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sat, 7 Nov 2026 11:42:25 +0100 Subject: [PATCH 318/322] fix(js/ts/python): support explicit constructors with as and then (#5235) --- src/Fable.Transforms/FSharp2Fable.fs | 150 ++++++++++- .../Compiler/CompilerMessagesTests.fs | 248 ++++++++++++++++++ .../Compiler/WatchDependencyTests.fs | 32 +++ .../Integration/CompilationTests.fs | 45 ++++ .../precompiledBases/selfBase/App.fs | 6 + .../selfBase/library/Library.fs | 7 + .../selfBase/library/library.fsproj | 12 + .../precompiledBases/selfBase/selfBase.fsproj | 13 + .../precompiledBases/staticInit/App.fs | 4 + .../staticInit/library/Library.fs | 6 + .../staticInit/library/library.fsproj | 12 + .../staticInit/staticInit.fsproj | 13 + tests/Js/Main/TypeTests.fs | 82 ++++++ tests/Python/TestType.fs | 87 ++++++ 14 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Integration/precompiledBases/selfBase/App.fs create mode 100644 tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs create mode 100644 tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj create mode 100644 tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj create mode 100644 tests/Integration/Integration/precompiledBases/staticInit/App.fs create mode 100644 tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs create mode 100644 tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj create mode 100644 tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 37e6eb62d8..ee731140ff 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -525,6 +525,7 @@ let private transformObjExpr match baseCall.DeclaringEntity with | Some baseEnt when baseEnt.TryFullName <> Some Types.object -> let r = makeRangeFrom baseCallExpr + checkSelfConstructorBase com ctx r baseEnt let genArgs = genArgs1 @ genArgs2 return transformBaseConsCall com ctx r baseEnt baseCall genArgs baseArgs |> Some @@ -793,6 +794,124 @@ let rec private transformDecisionTargets } +// F# omits the `init@` field it adds for `new() as x` from the record expression of the constructor +let private withOmittedFieldDefaults (com: IFableCompiler) (ctx: Context) (fsType: FSharpType) (args: Fable.Expr list) = + let ent = fsType.TypeDefinition + + if hasCompilerGeneratedConstructor ent then + let genArgs = + getGenericArguments fsType |> Seq.map (makeType ctx.GenericArgs) |> List.ofSeq + + let genArgs = + Seq.zip ent.GenericParameters genArgs + |> Seq.map (fun (param, typ) -> genParamName param, typ) + |> Map.ofSeq + + let fields = + ent.FSharpFields |> Seq.filter (fun field -> not field.IsStatic) |> List.ofSeq + + let omitted = + List.skip (min args.Length fields.Length) fields + |> List.map (fun field -> makeType genArgs field.FieldType |> Replacements.Api.defaultof com ctx None) + + args @ omitted + else + args + +let private (|LoadSelf|_|) (e: FSharpExpr) = + match e with + | FSharpExprPatterns.ILAsm(("[I_ldarg 0us]" | "[I_ldarg 0]"), [], []) -> Some() + | _ -> None + +// FCS writes `init@` on self in every constructor of a class with a self identifier +let rec private loadsSelf skipInitWrite (e: FSharpExpr) = + match e with + | LoadSelf -> true + | FSharpExprPatterns.FSharpFieldSet(Some LoadSelf, _, field, value) when + skipInitWrite && field.Name.StartsWith("init@") + -> + loadsSelf skipInitWrite value + | e -> e.ImmediateSubExpressions |> List.exists (loadsSelf skipInitWrite) + +let private hasSelfConstructor (com: IFableCompiler) (ent: FSharpEntity) = + let constructors = + ent.MembersFunctionsAndValues |> Seq.filter (fun memb -> memb.IsConstructor) + + let hasPrimaryConstructor = + constructors |> Seq.exists (fun memb -> memb.IsImplicitConstructor) + + let hasExplicitConstructor = + constructors |> Seq.exists (fun memb -> not memb.IsImplicitConstructor) + + if + (FsEnt.Ref ent).SourcePath.IsNone + || isGlobalOrImportedFSharpEntity ent + || not hasExplicitConstructor + then + false + else + let file = Path.normalizePathAndEnsureFsExtension ent.DeclarationLocation.FileName + + let rec exists decls = + decls + |> List.exists ( + function + | FSharpImplementationFileDeclaration.Entity(_, sub) -> exists sub + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, _, body) -> + memb.IsConstructor + && not memb.IsImplicitConstructor + && memb.DeclaringEntity + |> Option.exists (fun declaring -> declaring.TryFullName = ent.TryFullName) + && loadsSelf hasPrimaryConstructor body + | FSharpImplementationFileDeclaration.InitAction _ -> false + ) + + let decls = + try + Some(com.GetImplementationFile(file)) + with _ -> + None + + match decls with + | Some decls -> + com.AddWatchDependency(file) + exists decls + // Precompiled libraries keep the `init@` field of such a class, but not the constructor bodies + | None -> + ent.FSharpFields + |> Seq.exists (fun field -> not field.IsStatic && field.Name.StartsWith("init@")) + +let private checkSelfConstructorBase (com: IFableCompiler) (ctx: Context) r (baseEnt: FSharpEntity) = + if hasSelfConstructor com baseEnt then + $"%s{baseEnt.FullName} has a constructor with a self identifier (`new() as x`), which Fable does not support inheriting from." + |> addError com ctx.InlinePath r + +// F# emits `new() = { ... } then body` as sequences nested after the created object +let private (|ExplicitConstructorThen|_|) com (ctx: Context) (fsExpr: FSharpExpr) = + match ctx.EnclosingMember with + | Some m when + m.IsConstructor + && not m.IsImplicitConstructor + && supportsCompilerGeneratedBaseCall com + && m.DeclaringEntity + |> Option.exists (fun ent -> (tryGetBaseEntity ent).IsNone && not (isAttachMembersEntity com ent)) + -> + let isCreated (e: FSharpExpr) = + e.Type.HasTypeDefinition + && m.DeclaringEntity |> Option.exists (fun ent -> ent = e.Type.TypeDefinition) + + let rec split (e: FSharpExpr) = + match e with + | FSharpExprPatterns.Sequential(first, second) when isCreated first -> + let created, rest = split first + created, rest @ [ second ] + | _ -> e, [] + + match fsExpr with + | FSharpExprPatterns.Sequential(first, _) when isCreated fsExpr && isCreated first -> Some(split fsExpr) + | _ -> None + | _ -> None + let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fsExpr = trampoline { match fsExpr with @@ -987,6 +1106,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | _, Some i -> identWithRange r i |> Fable.IdentExpr | _, None -> Fable.Value(makeType Map.empty typ |> Fable.ThisValue, r) + // The `x` of `new() as x` is read through an IL `this` load, printed `0us` by FCS on .NET and `0` by FCS on JS + | FSharpExprPatterns.ILAsm(("[I_ldarg 0us]" | "[I_ldarg 0]"), [], []) -> + let r = makeRangeFrom fsExpr + + return + match ctx.BoundConstructorThis with + | Some i -> identWithRange r i |> Fable.IdentExpr + | None -> + "This explicit constructor uses a self identifier (`new() as x`), which Fable only supports on JavaScript, TypeScript and Python, in a class with no base class and without []." + |> addErrorAndReturnNull com ctx.InlinePath r + | FSharpExprPatterns.Value var -> let r = makeRangeFrom fsExpr @@ -1640,6 +1770,15 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return Fable.Let(baseIdent, baseValue, newRecord) + | ExplicitConstructorThen com ctx (created, rest) when ctx.BoundConstructorThis.IsNone -> + let! created = transformExpr com ctx [] created + let thisIdent = getIdentUniqueName com ctx "this" |> makeTypedIdent created.Type + + let! rest = + transformExprList com { ctx with BoundConstructorThis = Some thisIdent } rest + + return Fable.Let(thisIdent, created, Fable.Sequential(rest @ [ Fable.IdentExpr thisIdent ])) + | FSharpExprPatterns.Sequential(first, second) -> let exprs = match ctx.CaptureBaseConsCall with @@ -1666,7 +1805,10 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.NewRecord(fsType, argExprs) -> let r = makeRangeFrom fsExpr let! argExprs = transformExprList com ctx argExprs - let argExprs = withDefaultValueFields com ctx fsType argExprs + + let argExprs = + withDefaultValueFields com ctx fsType argExprs + |> withOmittedFieldDefaults com ctx fsType let genArgs = makeTypeGenArgs ctx.GenericArgs (getGenericArguments fsType) let entRef = FsEnt.Ref fsType.TypeDefinition @@ -2456,6 +2598,12 @@ let rec private transformDeclarations (com: FableCompiler) ctx fsDecls = checkInlinedTaggedUnionCases com ctx fsEnt warnDuplicateCompiledNames com ctx fsEnt + if not (isGlobalOrImportedFSharpEntity fsEnt) then + tryGetBaseEntity fsEnt + |> Option.iter (fun (baseEnt, _) -> + checkSelfConstructorBase com ctx (Some(makeRange fsEnt.DeclarationLocation)) baseEnt + ) + match sub with | [] when isIgnoredLeafEntity fsEnt -> [] | [] -> diff --git a/tests/Integration/Compiler/CompilerMessagesTests.fs b/tests/Integration/Compiler/CompilerMessagesTests.fs index c998d034b8..7fe26be375 100644 --- a/tests/Integration/Compiler/CompilerMessagesTests.fs +++ b/tests/Integration/Compiler/CompilerMessagesTests.fs @@ -21,6 +21,254 @@ let tests = |> Assert.Is.success |> ignore + testCase "Explicit constructor with a self identifier and no base compiles" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Explicit child of a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +type Derived = + inherit Base + val X: int + new () = { inherit Base(1); X = 1 } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Primary constructor child of a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +type Derived() = + inherit Base(5) +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Object expression over a class with a self identifier results in error" <| fun _ -> + let source = + """ +type Base = + val mutable Count: int + new (a: int) as self = { Count = a } then self.Count <- self.Count + 1 + +let o = { new Base(5) with member _.ToString() = "o" } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Child of a primary constructor class with a secondary self constructor results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member val V = a with get, set + new () as self = Base(1) then self.V <- self.V + 10 + +type Derived = + inherit Base + val E: int + new () = { inherit Base(); E = 1 } +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Self identifier with an explicit constructor base results in error" <| fun _ -> + let source = + """ +type Base = + val N: int + new (n: int) = { N = n } + +type Derived = + inherit Base + val mutable Y: int + new () as self = { inherit Base(1); Y = 0 } then self.Y <- self.N +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with a primary constructor base results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member _.A = a + +type Derived = + inherit Base + val mutable Y: int + new (a: int) as self = { inherit Base(a); Y = 0 } then self.Y <- self.A + 1 +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with a BCL base results in error" <| fun _ -> + let source = + """ +type Derived = + inherit System.Exception + val Code: int + new (m: string) as self = { inherit System.Exception(m); Code = 1 } +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Self identifier with AttachMembers results in error" <| fun _ -> + let source = + """ +[] +type C = + val mutable V: int + new (v: int) as self = { V = v } then self.V <- self.V + 1 +""" + compile source + |> Assert.Exists.errorWith "uses a self identifier" + |> ignore + + testCase "Child of a primary constructor class with a self identifier compiles" <| fun _ -> + let source = + """ +type Base() as this = + let mutable n = 0 + do this.Set(1) + member _.Set(v: int) = n <- v + member _.N = n + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Child of a primary constructor class with a self identifier and a secondary constructor compiles" <| fun _ -> + let source = + """ +type Base(x: int) as this = + do this.Touch() + new () = Base(1) + member _.X = x + member _.Touch() = () + +type Derived() = + inherit Base(2) +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Primary constructor child of a secondary constructor with a self identifier results in error" <| fun _ -> + let source = + """ +type Base(a: int) = + member val V = a with get, set + new () as self = Base(1) then self.V <- self.V + 10 + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + + testCase "Child of an imported class with a self identifier compiles" <| fun _ -> + let source = + """ +[] +type Base = + val mutable X: int + new () as self = { X = 1 } then self.X <- 2 + +type Derived() = + inherit Base() +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Imported child of a class with a self identifier compiles" <| fun _ -> + let source = + """ +type Base = + val mutable X: int + new () as self = { X = 1 } then self.X <- 2 + +[] +type Child() = + inherit Base() +""" + compile source + |> Assert.Is.success + |> ignore + + testCase "Explicit child of a class without a self identifier compiles" <| fun _ -> + let source = + """ +type Base = + val N: int + new (n: int) = { N = n } + +type Derived = + inherit Base + val X: int + new () = { inherit Base(1); X = 1 } +""" + compile source + |> Assert.Is.strictSuccess + |> ignore + + testCase "Child of a self identifier class behind a signature file results in error" <| fun _ -> + let signature = + """ +module Library + +type Base = + val mutable N: int + new: n: int -> Base +""" + + let library = + """ +module Library + +type Base = + val mutable N: int + new (n: int) as self = { N = n } then self.N <- self.N + 1 +""" + + let source = + """ +type Derived = + inherit Library.Base + val X: int + new () = { inherit Library.Base(1); X = 1 } +""" + + compileWithSignedLibrary signature library source + |> Assert.Exists.errorWith "does not support inheriting from" + |> ignore + testCase "Compile printfn" <| fun _ -> let source = "printfn \"Hello %s\" \"World\"" compile source diff --git a/tests/Integration/Compiler/WatchDependencyTests.fs b/tests/Integration/Compiler/WatchDependencyTests.fs index d35d4bd846..b913e89aeb 100644 --- a/tests/Integration/Compiler/WatchDependencyTests.fs +++ b/tests/Integration/Compiler/WatchDependencyTests.fs @@ -47,6 +47,38 @@ let tests = logs |> Assert.Is.strictSuccess |> ignore deps |> Map.tryFind "Program.fs" |> Option.defaultValue [] |> equal [ "Library.fs" ] + testCase "A file that inherits a parameterless explicit constructor depends on the file of its body" <| fun _ -> + let signature = + """ +module Library + +type Base = + val N: int + new: unit -> Base +""" + + let library = + """ +module Library + +type Base = + val N: int + new () = { N = 1 } +""" + + let source = + """ +type Derived = + inherit Library.Base + val M: int + new () = { inherit Library.Base(); M = 2 } +""" + + let logs, deps = watchDependencies signature library source + + logs |> Assert.Is.strictSuccess |> ignore + deps |> Map.tryFind "Program.fs" |> Option.defaultValue [] |> equal [ "Library.fs" ] + testCase "Editing an inline function in a file with a signature recompiles the files that inlined it" <| fun _ -> Compiler.Cached.startWatchingSignedLibrary Compiler.Settings.standard signature (library 3) source waitForOutput "Program.fs" "2 * 3" diff --git a/tests/Integration/Integration/CompilationTests.fs b/tests/Integration/Integration/CompilationTests.fs index d1bc6070b4..180dfa25e3 100644 --- a/tests/Integration/Integration/CompilationTests.fs +++ b/tests/Integration/Integration/CompilationTests.fs @@ -56,6 +56,46 @@ let private checkBrowserInlineExprs (precompiledDir: string) = (Fable.BrowserInlineExprs.toString [| name, fromCompact |]) $"%s{name} differs between the two copies" +let private precompiledBaseTest name fixture shouldCompile = + testCaseAsync + name + (async { + let testCaseDir = Path.Combine(__SOURCE_DIRECTORY__, "precompiledBases", fixture) + let libraryProject = Path.Combine(testCaseDir, "library", "library.fsproj") + let project = Path.Combine(testCaseDir, fixture + ".fsproj") + let tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + let precompiledDir = Path.Combine(tempDir, "precompiled") + + try + let precompileExitCode = + Fable.Cli.Entry.main [| "precompile"; libraryProject; "--outDir"; precompiledDir; "--noCache" |] + + Expect.equal precompileExitCode 0 "Expected precompile exit code to be 0" + + let exitCode = + Fable.Cli.Entry.main + [| + project + "--cwd" + testCaseDir + "--outDir" + Path.Combine(tempDir, "out") + "--exclude" + "Fable.Core" + "--noCache" + "--precompiledLib" + precompiledDir + |] + + if shouldCompile then + Expect.equal exitCode 0 "Expected the compilation to succeed" + else + Expect.notEqual exitCode 0 "Expected the compilation to fail" + finally + if Directory.Exists tempDir then + Directory.Delete(tempDir, true) + }) + let tests = Directory.EnumerateDirectories(data) |> Seq.map (fun testCaseDir -> // @@ -129,4 +169,9 @@ let tests = })) |> Seq.toList + |> List.append + [ + precompiledBaseTest "Inheriting a precompiled class with a self identifier is an error" "selfBase" false + precompiledBaseTest "Inheriting a precompiled class with a static let binding compiles" "staticInit" true + ] |> testList "Compilation" diff --git a/tests/Integration/Integration/precompiledBases/selfBase/App.fs b/tests/Integration/Integration/precompiledBases/selfBase/App.fs new file mode 100644 index 0000000000..1b320a101b --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/App.fs @@ -0,0 +1,6 @@ +module App + +type Derived = + inherit Library.Base + val M: int + new () = { inherit Library.Base(1); M = 2 } diff --git a/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs b/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs new file mode 100644 index 0000000000..f520508840 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/library/Library.fs @@ -0,0 +1,7 @@ +module Library + +let registered = ResizeArray() + +type Base = + val N: int + new (n: int) as self = { N = n } then registered.Add(box self) diff --git a/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj b/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj new file mode 100644 index 0000000000..6fc5f67a94 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/library/library.fsproj @@ -0,0 +1,12 @@ + + + net10.0 + Major + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj b/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj new file mode 100644 index 0000000000..912d3e71a2 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/selfBase/selfBase.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + Major + + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/staticInit/App.fs b/tests/Integration/Integration/precompiledBases/staticInit/App.fs new file mode 100644 index 0000000000..9a0033d8aa --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/App.fs @@ -0,0 +1,4 @@ +module App + +type Child() = + inherit Library.Counter(1) diff --git a/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs b/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs new file mode 100644 index 0000000000..7fa0cb4149 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/library/Library.fs @@ -0,0 +1,6 @@ +module Library + +type Counter(n: int) = + static let zero = 0 + new () = Counter(zero) + member _.N = n diff --git a/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj b/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj new file mode 100644 index 0000000000..6fc5f67a94 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/library/library.fsproj @@ -0,0 +1,12 @@ + + + net10.0 + Major + + + + + + + + diff --git a/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj b/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj new file mode 100644 index 0000000000..912d3e71a2 --- /dev/null +++ b/tests/Integration/Integration/precompiledBases/staticInit/staticInit.fsproj @@ -0,0 +1,13 @@ + + + net10.0 + Major + + + + + + + + + diff --git a/tests/Js/Main/TypeTests.fs b/tests/Js/Main/TypeTests.fs index 4f8b8e5bde..f646f7f8d5 100644 --- a/tests/Js/Main/TypeTests.fs +++ b/tests/Js/Main/TypeTests.fs @@ -975,6 +975,50 @@ type ValClassPairBox<'U> = val Count: int new (a: 'U, b: 'U) = { inherit ValClassGenericBase<'U * 'U>((a, b)); Count = 2 } +let mutable valClassThenCalls = 0 + +type ValClassThen = + val mutable Value: int + new () = { Value = 1 } then valClassThenCalls <- valClassThenCalls + 1 + +[] +type ValClassSequenceInit = + val Next: ValClassSequenceInit + val Id: int + val Boxed: obj + new (id: int) = { Next = null; Id = id; Boxed = null } + new (a: int, b: int) = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = 0; Boxed = box (do ValClassSequenceInit(a)) } + new (a: int, b: int, c: int) as self = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = c; Boxed = box (do ValClassSequenceInit(a)) } then self.Next.Id |> ignore + +type ValClassSelf = + val mutable Name: string + val mutable Count: int + new (name: string) as self = { Name = name; Count = 0 } then self.Count <- self.Name.Length + new () as self = ValClassSelf("ab") then self.Count <- self.Count * 10 + +type ValClassSelfOutOfOrder = + val Z: int + val mutable W: int + new () as self = { W = 2; Z = 3 } then self.W <- self.W + self.Z + +let valClassSelfSaved = ResizeArray() + +type ValClassSelfSaved = + val Id: int + new (id: int) as self = { Id = id } then valClassSelfSaved.Add(box self) + new () as self = ValClassSelfSaved(1) then valClassSelfSaved.Add(box self) + +type SelfPrimaryBase(x: int) as this = + let mutable touched = 0 + do this.Touch() + new () = SelfPrimaryBase(1) + member _.X = x + member _.Touched = touched + member _.Touch() = touched <- touched + 1 + +type SelfPrimaryChild() = + inherit SelfPrimaryBase(2) + let tests = testList "Types" [ @@ -1989,4 +2033,42 @@ let tests = let pairBox = ValClassPairBox("a", "b") pairBox.Value |> equal ("a", "b") pairBox.Count |> equal 2 + + testCase "Explicit constructor returns the object after its then block" <| fun () -> + let before = valClassThenCalls + let c = ValClassThen() + c.Value |> equal 1 + valClassThenCalls - before |> equal 1 + + testCase "Explicit constructor keeps sequences of its own constructor calls in field values" <| fun () -> + let c = ValClassSequenceInit(5, 6) + c.Next.Id |> equal 6 + isNull c.Boxed |> equal true + let d = ValClassSequenceInit(5, 6, 7) + (d.Next.Id, d.Id, isNull d.Boxed) |> equal (6, 7, true) + + testCase "Explicit constructor with a self identifier works" <| fun () -> // See #5166 + let a = ValClassSelf("abc") + a.Name |> equal "abc" + a.Count |> equal 3 + let b = ValClassSelf() + b.Name |> equal "ab" + b.Count |> equal 20 + let c = ValClassSelfOutOfOrder() + (c.Z, c.W) |> equal (3, 5) + + testCase "Primary constructor with a self identifier works with a secondary constructor" <| fun () -> + let a = SelfPrimaryBase() + (a.X, a.Touched) |> equal (1, 1) + let b = SelfPrimaryChild() + (b.X, b.Touched) |> equal (2, 1) + + testCase "Explicit constructor self identifier is the created object" <| fun () -> + valClassSelfSaved.Clear() + let a = ValClassSelfSaved(5) + obj.ReferenceEquals(valClassSelfSaved.[0], a) |> equal true + let b = ValClassSelfSaved() + valClassSelfSaved.Count |> equal 3 + obj.ReferenceEquals(valClassSelfSaved.[1], b) |> equal true + obj.ReferenceEquals(valClassSelfSaved.[2], b) |> equal true ] diff --git a/tests/Python/TestType.fs b/tests/Python/TestType.fs index a2eef539b1..ab9fc9476c 100644 --- a/tests/Python/TestType.fs +++ b/tests/Python/TestType.fs @@ -2030,3 +2030,90 @@ let ``test explicit constructor resolves the generic arguments of the base class let pairBox = ValClassPairBox("a", "b") pairBox.Value |> equal ("a", "b") pairBox.Count |> equal 2 + +let mutable valClassThenCalls = 0 + +type ValClassThen = + val mutable Value: int + new () = { Value = 1 } then valClassThenCalls <- valClassThenCalls + 1 + +[] +type ValClassSequenceInit = + val Next: ValClassSequenceInit + val Id: int + val Boxed: obj + new (id: int) = { Next = null; Id = id; Boxed = null } + new (a: int, b: int) = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = 0; Boxed = box (do ValClassSequenceInit(a)) } + new (a: int, b: int, c: int) as self = { Next = (ValClassSequenceInit(a); ValClassSequenceInit(b)); Id = c; Boxed = box (do ValClassSequenceInit(a)) } then self.Next.Id |> ignore + +type ValClassSelf = + val mutable Name: string + val mutable Count: int + new (name: string) as self = { Name = name; Count = 0 } then self.Count <- self.Name.Length + new () as self = ValClassSelf("ab") then self.Count <- self.Count * 10 + +type ValClassSelfOutOfOrder = + val Z: int + val mutable W: int + new () as self = { W = 2; Z = 3 } then self.W <- self.W + self.Z + +let valClassSelfSaved = ResizeArray() + +type ValClassSelfSaved = + val Id: int + new (id: int) as self = { Id = id } then valClassSelfSaved.Add(box self) + new () as self = ValClassSelfSaved(1) then valClassSelfSaved.Add(box self) + +type SelfPrimaryBase(x: int) as this = + let mutable touched = 0 + do this.Touch() + new () = SelfPrimaryBase(1) + member _.X = x + member _.Touched = touched + member _.Touch() = touched <- touched + 1 + +type SelfPrimaryChild() = + inherit SelfPrimaryBase(2) + +[] +let ``test explicit constructor returns the object after its then block`` () = + let before = valClassThenCalls + let c = ValClassThen() + c.Value |> equal 1 + valClassThenCalls - before |> equal 1 + +[] +let ``test explicit constructor keeps sequences of its own constructor calls in field values`` () = + let c = ValClassSequenceInit(5, 6) + c.Next.Id |> equal 6 + isNull c.Boxed |> equal true + let d = ValClassSequenceInit(5, 6, 7) + (d.Next.Id, d.Id, isNull d.Boxed) |> equal (6, 7, true) + +[] +let ``test explicit constructor with a self identifier works`` () = // See #5166 + let a = ValClassSelf("abc") + a.Name |> equal "abc" + a.Count |> equal 3 + let b = ValClassSelf() + b.Name |> equal "ab" + b.Count |> equal 20 + let c = ValClassSelfOutOfOrder() + (c.Z, c.W) |> equal (3, 5) + +[] +let ``test primary constructor with a self identifier works with a secondary constructor`` () = + let a = SelfPrimaryBase() + (a.X, a.Touched) |> equal (1, 1) + let b = SelfPrimaryChild() + (b.X, b.Touched) |> equal (2, 1) + +[] +let ``test explicit constructor self identifier is the created object`` () = + valClassSelfSaved.Clear() + let a = ValClassSelfSaved(5) + obj.ReferenceEquals(valClassSelfSaved.[0], a) |> equal true + let b = ValClassSelfSaved() + valClassSelfSaved.Count |> equal 3 + obj.ReferenceEquals(valClassSelfSaved.[1], b) |> equal true + obj.ReferenceEquals(valClassSelfSaved.[2], b) |> equal true From 62378c8bd5bfdaed907df11619a3362b86dd522b Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Sat, 7 Nov 2026 12:56:18 +0100 Subject: [PATCH 319/322] fix(js/ts): evaluate for loop bound once (#5282) --- src/Fable.Transforms/Babel/Fable2Babel.fs | 91 +++++++- tests/Js/Main/ImportTests.fs | 14 ++ tests/Js/Main/JsInteropTests.fs | 21 ++ tests/Js/Main/MiscTests.fs | 271 ++++++++++++++++++++++ tests/Js/Main/MiscTestsHelper.fs | 8 + tests/Js/Main/js/1foo.js | 2 + 6 files changed, 398 insertions(+), 9 deletions(-) diff --git a/src/Fable.Transforms/Babel/Fable2Babel.fs b/src/Fable.Transforms/Babel/Fable2Babel.fs index 5582a9ae22..84a762fb02 100644 --- a/src/Fable.Transforms/Babel/Fable2Babel.fs +++ b/src/Fable.Transforms/Babel/Fable2Babel.fs @@ -1138,17 +1138,70 @@ module Util = ctx.UsedNames.RootScope.Add(name) |> ignore name - let getUniqueNameInDeclarationScope (ctx: Context) name = + let getUniqueNameInDeclarationScopeWith (ctx: Context) (isUsed: string -> bool) name = let name = (name, Naming.NoMemberPart) ||> Naming.sanitizeJsIdent (fun name -> ctx.UsedNames.RootScope.Contains(name) || ctx.UsedNames.CurrentDeclarationScope.Contains(name) + || isUsed name ) ctx.UsedNames.CurrentDeclarationScope.Add(name) |> ignore name + let getUniqueNameInDeclarationScope (ctx: Context) name = + getUniqueNameInDeclarationScopeWith ctx (fun _ -> false) name + + let rec isStableForLoopLimit (limit: Fable.Expr) = + match limit with + | Fable.Value(Fable.NumberConstant _, _) -> true + | Fable.IdentExpr ident -> not ident.IsMutable + | Fable.Get(Fable.IdentExpr ident, Fable.FieldGet info, _, _) when info.Name = "length" -> + match ident.Type with + | Fable.String + | Fable.Array(_, (Fable.MutableArray | Fable.ImmutableArray)) -> not ident.IsMutable + | _ -> false + | Fable.Operation(Fable.Unary(_, operand), _, _, _) -> isStableForLoopLimit operand + | Fable.Operation(Fable.Binary(_, left, right), _, _, _) -> + isStableForLoopLimit left && isStableForLoopLimit right + | _ -> false + + let private isMutableArrayLength (com: Compiler) (expr: Fable.Expr) = + let isArray = + function + | Fable.Array(_, (Fable.MutableArray | Fable.ImmutableArray)) -> true + | _ -> false + + let isGetterCall (callInfo: Fable.CallInfo) = + Option.isNone callInfo.ThisArg && List.isEmpty callInfo.Args + + match expr with + | Fable.Get(Fable.IdentExpr ident, Fable.FieldGet info, _, _) when info.Name = "length" -> + ident.IsMutable && isArray ident.Type + | Fable.Get(Fable.Call(Fable.IdentExpr ident, callInfo, _, _), Fable.FieldGet info, _, _) when + info.Name = "length" && isGetterCall callInfo + -> + ident.IsMutable && isArray ident.Type + | Fable.Get(Fable.Call(Fable.Import({ Kind = Fable.MemberImport memberRef }, _, _), callInfo, typ, _), + Fable.FieldGet info, + _, + _) when info.Name = "length" && isGetterCall callInfo && isArray typ -> + match com.TryGetMember(memberRef) with + | Some memb -> memb.IsValue && memb.IsMutable + | None -> false + | _ -> false + + let isReReadForLoopLimit (com: Compiler) isUp (limit: Fable.Expr) = + match limit with + | Fable.Operation(Fable.Binary(BinaryMinus, + length, + Fable.Value(Fable.NumberConstant(Fable.NumberValue.Int32 1, _), _)), + _, + _, + _) when isUp -> isMutableArrayLength com length + | _ -> false + type NamedTailCallOpportunity(_com: Compiler, ctx, name, args: Fable.Ident list) = // Capture the current argument values to prevent delayed references from getting corrupted, // for that we use block-scoped ES2015 variable declarations. See #681, #1859 @@ -3544,25 +3597,45 @@ but thanks to the optimisation done below we get let body = transformBlockWithVarHoisting com ctx None body [| Statement.whileStatement (guard, body, ?loc = range) |] - | Fable.ForLoop(var, TransformExpr com ctx start, TransformExpr com ctx limit, body, isUp, range) -> + | Fable.ForLoop(var, fableStart, fableLimit, fableBody, isUp, range) -> + let start = transformAsExpr com ctx fableStart + let limit = transformAsExpr com ctx fableLimit + // Hoist vars within the loop so if a closure captures the variable + // it doesn't change in the next iteration, see #4031 + let body = transformBlockWithVarHoisting com ctx None fableBody + let op1, op2 = if isUp then BinaryOperator.BinaryLessOrEqual, UpdateOperator.UpdatePlus else BinaryOperator.BinaryGreaterOrEqual, UpdateOperator.UpdateMinus + let makeDeclarator name init = + VariableDeclarator.variableDeclarator ( + name, + init = init, + ?annotation = makeTypeAnnotationIfTypeScript com ctx var.Type (Some init) + ) + + let limitDeclarators, limitExpr = + if isStableForLoopLimit fableLimit || isReReadForLoopLimit com isUp fableLimit then + [||], limit + else + let isUsed name = + [ fableStart; fableLimit; fableBody ] |> List.exists (isIdentUsed name) + + let limitName = getUniqueNameInDeclarationScopeWith ctx isUsed "limit" + + [| makeDeclarator limitName limit |], Expression.identifier limitName + [| Statement.forStatement ( - // Hoist vars within the loop so if a closure captures the variable - // it doesn't change in the next iteration, see #4031 - transformBlockWithVarHoisting com ctx None body, + body, VariableDeclaration.variableDeclaration ( Let, - var.Name, - init = start, - ?annotation = makeTypeAnnotationIfTypeScript com ctx var.Type (Some start) + Array.append [| makeDeclarator var.Name start |] limitDeclarators ), - Expression.binaryExpression (op1, identAsExpr var, limit), + Expression.binaryExpression (op1, identAsExpr var, limitExpr), Expression.updateExpression (op2, false, identAsExpr var), ?loc = range ) diff --git a/tests/Js/Main/ImportTests.fs b/tests/Js/Main/ImportTests.fs index 50b8f46030..0a9ff9545f 100644 --- a/tests/Js/Main/ImportTests.fs +++ b/tests/Js/Main/ImportTests.fs @@ -99,6 +99,17 @@ type Treasure = let myKeyInlineWrapper() = Treasure.keyHolderInline + +[] +let importedLimit: int = jsNative + +let private forLoopBoundThree () = 3 + +let forLoopBoundWithImportedLimit () = + let mutable sum = 0 + for _ = 0 to forLoopBoundThree () do + sum <- sum + importedLimit + sum #endif let tests = @@ -117,6 +128,9 @@ let tests = let fooAll2: IFooImported = JsInterop.importAll "./js/1foo.js" fooAll2.foo |> equal "foo" + testCase "for bound does not shadow an import" <| fun () -> + forLoopBoundWithImportedLimit () |> equal 400 + testCase "Symbols in external projects work" <| fun () -> equal "Fable Rocks!" Spaces.Helper.ConditionalExternalValue diff --git a/tests/Js/Main/JsInteropTests.fs b/tests/Js/Main/JsInteropTests.fs index 7f0fd3b346..360686ffef 100644 --- a/tests/Js/Main/JsInteropTests.fs +++ b/tests/Js/Main/JsInteropTests.fs @@ -247,6 +247,19 @@ type ParseSpeeds = | [] Medium | [] Slow +#if !FABLE_COMPILER_TYPESCRIPT +[] +let globalLimit: int = jsNative + +let private forLoopBoundThree () = 3 + +let forLoopBoundWithGlobalLimit () = + let mutable sum = 0 + for _ = 0 to forLoopBoundThree () do + sum <- sum + globalLimit + sum +#endif + [] #endif type Field = OldPassword | NewPassword | ConfirmPassword @@ -679,6 +692,14 @@ let tests = instance.regular 3 |> equal 4 #if FABLE_COMPILER +#if !FABLE_COMPILER_TYPESCRIPT + testCase "for bound does not shadow a global" <| fun () -> + emitJsStatement () "Reflect.set(globalThis, 'limit', 21)" + let result = forLoopBoundWithGlobalLimit () + emitJsStatement () "Reflect.deleteProperty(globalThis, 'limit')" + equal 84 result +#endif + testCase "Can type test interfaces decorated with Global" <| fun () -> let ar = ResizeArray [| 1; 2; 3 |] |> box match ar with diff --git a/tests/Js/Main/MiscTests.fs b/tests/Js/Main/MiscTests.fs index a8e5163f7e..3cf26f5f4e 100644 --- a/tests/Js/Main/MiscTests.fs +++ b/tests/Js/Main/MiscTests.fs @@ -532,6 +532,42 @@ type DocCommentClass() = /// Member doc */ member _.Get() = 1 +let mutable private forLoopModuleBound = 3 + +type ForLoopBound() = + let mutable bound = 3 + + member val Bound = 3 with get, set + + member _.FieldBoundIterations() = + let mutable n = 0 + for _ = 0 to bound do + bound <- 1 + n <- n + 1 + n + + member this.PropertyBoundIterations() = + let mutable n = 0 + for _ = 0 to this.Bound do + this.Bound <- 1 + n <- n + 1 + n + +let rec private forLoopBoundTailCall (n: int) acc = + if n = 0 then + acc + else + let xs = ResizeArray [ for _ in 1 .. n -> 1 ] + let mutable c = 0 + for _ = 0 to xs.Count - 1 do + if xs.Count < 20 then + xs.Add 0 + c <- c + 1 + forLoopBoundTailCall (n - 1) (acc + c) + +let mutable forLoopPublicArray = [| 1; 2; 3 |] +let mutable private forLoopPrivateArray = [| 1; 2; 3 |] + let tests = testList "Miscellaneous" [ testCase "Hoisted vars don't conflict with while loop" <| fun () -> // see #4031 @@ -761,6 +797,241 @@ let tests = fns.Add(fun () -> i) fns |> Seq.map (fun f -> f ()) |> Seq.toArray |> equal [| 3; 2; 1 |] + testCase "for bound over a collection is evaluated once" <| fun () -> + let shrinking = ResizeArray [ 1; 2; 3; 4 ] + let mutable n = 0 + for _ in 0 .. shrinking.Count - 1 do + if shrinking.Count > 2 then + shrinking.RemoveAt 0 + n <- n + 1 + equal 4 n + let growing = ResizeArray [ 1; 2 ] + let mutable m = 0 + for _ = 0 to growing.Count - 1 do + if growing.Count < 8 then + growing.Add 9 + m <- m + 1 + equal 2 m + + testCase "for bound with side effects is evaluated once" <| fun () -> + let calls = ref 0 + let hi () = + calls.Value <- calls.Value + 1 + 2 + for _ in 0 .. hi () do + () + equal 1 calls.Value + + testCase "for downto bound is evaluated once" <| fun () -> + let xs = ResizeArray [ 1; 2; 3; 4 ] + let mutable n = 0 + for _ = xs.Count - 1 downto xs.Count - 2 do + xs.RemoveAt 0 + n <- n + 1 + equal 2 n + let ys = ResizeArray [ 1; 2; 3 ] + let mutable m = 0 + for _ in 3 .. -1 .. ys.Count - 2 do + ys.Add 0 + m <- m + 1 + equal 3 m + + testCase "for bound reading a mutable local changed in the body is evaluated once" <| fun () -> + let mutable hi = 3 + let mutable a = 0 + for _ = 0 to hi do + hi <- 10 + a <- a + 1 + equal 4 a + let mutable lo = 3 + let mutable b = 0 + for _ = 5 downto lo do + lo <- 0 + b <- b + 1 + equal 3 b + let mutable x = 2 + let mutable c = 0 + for _ = 0 to x + 1 do + x <- 100 + c <- c + 1 + equal 4 c + let mutable str = "abc" + let mutable d = 0 + for _ = 0 to str.Length - 1 do + str <- "a" + d <- d + 1 + equal 3 d + + testCase "for bound reading a ref cell, a field or a module mutable is evaluated once" <| fun () -> + let r = ref 2 + let mutable a = 0 + for _ = 0 to r.Value do + r.Value <- 7 + a <- a + 1 + equal 3 a + ForLoopBound().FieldBoundIterations() |> equal 4 + ForLoopBound().PropertyBoundIterations() |> equal 4 + forLoopModuleBound <- 3 + let mutable b = 0 + for _ = 0 to forLoopModuleBound do + forLoopModuleBound <- 1 + b <- b + 1 + equal 4 b + + testCase "for start is evaluated before the bound and both only once" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + for _ = tick "start" 0 to tick "limit" 1 do + log.Add "body" + for _ = tick "start" 1 downto tick "limit" 0 do + log.Add "body" + String.concat "," log |> equal "start,limit,body,body,start,limit,body,body" + + testCase "Nested for bounds are each evaluated once per entry" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + let limit = 0 + for _ = 0 to tick "outer" 1 + limit do + for _ = 0 to tick "inner" 0 + limit do + let limit = 5 + log.Add(string limit) + String.concat "," log |> equal "outer,inner,5,inner,5" + + testCase "Closures in a for body work when the bound is evaluated once" <| fun () -> + let fns = ResizeArray int>() + let xs = ResizeArray [ 1; 2; 3 ] + for i = 0 to xs.Count - 1 do + let doubled = i * 2 + fns.Add(fun () -> doubled + i + xs.Count) + if xs.Count < 8 then + xs.Add 0 + fns |> Seq.map (fun f -> f ()) |> Seq.toArray |> equal [| 6; 9; 12 |] + + testCase "for bound is evaluated once in tail recursive functions" <| fun () -> + forLoopBoundTailCall 3 0 |> equal 6 + + testCase "for bound is evaluated once when the loop is in expression position" <| fun () -> + let growUpTo20 (xs: ResizeArray) = + if xs.Count < 20 then + xs.Add 0 + let xs = ResizeArray [ 1; 2 ] + let unitResult = (for _ = 0 to xs.Count - 1 do growUpTo20 xs) + equal () unitResult + equal 4 xs.Count + let ys = ResizeArray [ 1; 2 ] + let run enabled = + if enabled then + for _ = 0 to ys.Count - 1 do + growUpTo20 ys + run true + equal 4 ys.Count + let zs = ResizeArray [ 1; 2 ] + let count = + try + for _ = 0 to zs.Count - 1 do + growUpTo20 zs + zs.Count + with _ -> -1 + equal 4 count + + testCase "for bound that needs statements is evaluated once" <| fun () -> + let log = ResizeArray() + let tick name value = + log.Add name + value + let xs = ResizeArray [ 1; 2 ] + let mutable a = 0 + for _ = 0 to (match xs.Count with 2 -> tick "match" 3 | _ -> 0) do + if xs.Count < 20 then + xs.Add 0 + a <- a + 1 + equal 4 a + let mutable b = 0 + for _ = 0 to (let x = tick "let" 1 in x + tick "let2" 1) do + b <- b + 1 + equal 3 b + let mutable c = 0 + for _ = 0 to (try tick "try" 1 with _ -> 0) do + c <- c + 1 + equal 2 c + String.concat "," log |> equal "match,let,let2,try" + + testCase "for bound is evaluated once inside a seq expression" <| fun () -> + let count = + seq { + let xs = ResizeArray [ 1; 2 ] + let grow () = + for _ = 0 to xs.Count - 1 do + if xs.Count < 20 then + xs.Add 0 + xs.Count + yield grow () + } + |> Seq.head + equal 4 count + + testCase "for bound reading the length of a mutable array minus one is evaluated each iteration" <| fun () -> + let mutable a = [| 1; 2; 3 |] + let mutable n = 0 + for _ = 0 to a.Length - 1 do + a <- [| 1; 2; 3; 4; 5; 6 |] + n <- n + 1 + equal 6 n + forLoopPublicArray <- [| 1; 2; 3 |] + let mutable m = 0 + for _ = 0 to forLoopPublicArray.Length - 1 do + forLoopPublicArray <- [| 1; 2; 3; 4; 5; 6 |] + m <- m + 1 + equal 6 m + forLoopPrivateArray <- [| 1; 2; 3 |] + let mutable p = 0 + for _ = 0 to forLoopPrivateArray.Length - 1 do + forLoopPrivateArray <- [| 1; 2; 3; 4; 5; 6 |] + p <- p + 1 + equal 6 p + MiscTestsHelper.forLoopCrossFileArray <- [| 1; 2; 3 |] + let mutable q = 0 + for _ = 0 to MiscTestsHelper.forLoopCrossFileArray.Length - 1 do + MiscTestsHelper.forLoopCrossFileArray <- [| 1; 2; 3; 4; 5; 6 |] + q <- q + 1 + equal 6 q + MiscTestsHelper.forLoopCrossFileCalls <- 0 + let mutable r = 0 + for _ = 0 to (MiscTestsHelper.forLoopCrossFileMakeArray ()).Length - 1 do + r <- r + 1 + equal 3 r + equal 1 MiscTestsHelper.forLoopCrossFileCalls + + testCase "for bound reading the length of a mutable array in other shapes is evaluated once" <| fun () -> + let mutable a = [| 1; 2; 3 |] + let mutable n = 0 + for _ = 3 downto a.Length - 1 do + a <- [| 1 |] + n <- n + 1 + equal 2 n + a <- [| 1; 2; 3 |] + let mutable m = 0 + for _ = 0 to a.Length do + a <- [| 1 |] + m <- m + 1 + equal 4 m + a <- [| 1; 2; 3 |] + let mutable p = 0 + for _ = 0 to a.Length - 2 do + a <- [| 1 |] + p <- p + 1 + equal 2 p + a <- [| 1; 2; 3 |] + let mutable q = 0 + for _ = 0 to a.Length do + if q < 10 then a <- Array.append a [| 0 |] + q <- q + 1 + equal 4 q + testCase "Self references in constructors work" <| fun () -> // See #124 let t = Test(5) equal 12 t.Value diff --git a/tests/Js/Main/MiscTestsHelper.fs b/tests/Js/Main/MiscTestsHelper.fs index 3f20c76912..70ecf518dc 100644 --- a/tests/Js/Main/MiscTestsHelper.fs +++ b/tests/Js/Main/MiscTestsHelper.fs @@ -37,3 +37,11 @@ let mutable trackedCalls = 0 let track x = trackedCalls <- trackedCalls + 1 x + +let mutable forLoopCrossFileArray = [| 1; 2; 3 |] + +let mutable forLoopCrossFileCalls = 0 + +let forLoopCrossFileMakeArray () = + forLoopCrossFileCalls <- forLoopCrossFileCalls + 1 + [| 1; 2; 3 |] diff --git a/tests/Js/Main/js/1foo.js b/tests/Js/Main/js/1foo.js index 3a953f1b85..01fb60150f 100644 --- a/tests/Js/Main/js/1foo.js +++ b/tests/Js/Main/js/1foo.js @@ -2,6 +2,8 @@ export const foo = "foo"; +export const limit = 100; + export default { myKey: "a secret" } From 8c1bf9460c05aa8cd340843b84f4588ef07cfbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 7 Nov 2026 13:35:55 -0800 Subject: [PATCH 320/322] fix(dart): Run Expecto tests (#5288) --- src/Fable.Build/Test/Dart.fs | 7 ++- tests/Dart/main.dart | 71 -------------------------- tests/Dart/src/AsyncTests.fs | 4 ++ tests/Dart/src/Fable.Tests.Dart.fsproj | 4 +- tests/Dart/src/Main.fs | 54 ++++++++++++++++++++ tests/Dart/src/QuotationTests.fs | 4 ++ tests/Dart/src/RuneTests.fs | 4 ++ tests/Dart/src/StringTests.fs | 2 + tests/Dart/src/Tests.Util.fs | 53 +++++++++++++++++++ 9 files changed, 127 insertions(+), 76 deletions(-) delete mode 100644 tests/Dart/main.dart create mode 100644 tests/Dart/src/Main.fs diff --git a/src/Fable.Build/Test/Dart.fs b/src/Fable.Build/Test/Dart.fs index a4185bdf02..b4707b7976 100644 --- a/src/Fable.Build/Test/Dart.fs +++ b/src/Fable.Build/Test/Dart.fs @@ -26,8 +26,7 @@ let handle (args: string list) = Shell.copyFile buildDir (testsFolder "analysis_options.yaml") Directory.GetFiles(testsFolder, "*.dart") |> Seq.iter (Shell.copyFile buildDir) - - let testCmd = $"dart test {buildDir}/main.dart" + let testCmd = $"dart test {buildDir}/src/Main.dart" let fableArgs = CmdLine.concat @@ -52,7 +51,7 @@ let handle (args: string list) = Async.Parallel [ if not noDotnet then - Command.RunAsync("dotnet", "watch test -c Release", workingDirectory = testsFsprojFolder) + Command.RunAsync("dotnet", "watch run -c Release", workingDirectory = testsFsprojFolder) |> Async.AwaitTask Command.WatchFableAsync(fableArgs, workingDirectory = buildDir) @@ -61,6 +60,6 @@ let handle (args: string list) = |> Async.RunSynchronously |> ignore else - Command.Run("dotnet", "test -c Release", workingDirectory = testsFsprojFolder) + Command.Run("dotnet", "run -c Release", workingDirectory = testsFsprojFolder) Command.Fable(fableArgs, workingDirectory = buildDir) diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart deleted file mode 100644 index 3a2855f4a6..0000000000 --- a/tests/Dart/main.dart +++ /dev/null @@ -1,71 +0,0 @@ -import './src/ArithmeticTests.dart' as arithmetic; -import './src/AsyncTests.dart' as async_; -import './src/RandomTests.dart' as random; -import './src/ArrayTests.dart' as array; -import './src/ComparisonTests.dart' as comparison; -import './src/CustomOperatorTests.dart' as custom_operator; -import './src/DateTimeTests.dart' as date; -import './src/DictionaryTests.dart' as dictionary; -import './src/EnumTests.dart' as enum_; -import './src/EnumerableTests.dart' as enumerable; -import './src/HashSetTests.dart' as hash_set; -import './src/InlineIfLambdaTests.dart' as inline_if_lambda; -import './src/ListTests.dart' as list; -import './src/MapTests.dart' as map; -import './src/MiscTests.dart' as misc; -// import './src/NullnessTests.dart' as nullness; -import './src/NullableTests.dart' as nullable; -import './src/OptionTests.dart' as option; -import './src/QuotationTests.dart' as quotation; -import './src/RecordTests.dart' as record; -import './src/RegexTests.dart' as regex; -import './src/ResizeArrayTests.dart' as resize_array; -import './src/ResultTests.dart' as result; -import './src/RuneTests.dart' as rune; -import './src/SeqTests.dart' as seq; -import './src/SeqExpressionTests.dart' as seq_expression; -import './src/SetTests.dart' as set_; -import './src/StringTests.dart' as string; -import './src/SudokuTests.dart' as sudoku; -import './src/TailCallTests.dart' as tailcall; -import './src/TimeSpanTests.dart' as timespan; -import './src/TupleTests.dart' as tuple; -import './src/TypeTests.dart' as type_; -import './src/UnionTests.dart' as union; - -void main() { - arithmetic.tests(); - async_.tests(); - random.tests(); - array.tests(); - comparison.tests(); - custom_operator.tests(); - date.tests(); - dictionary.tests(); - enum_.tests(); - enumerable.tests(); - hash_set.tests(); - inline_if_lambda.tests(); - list.tests(); - map.tests(); - misc.tests(); - // nullness.tests(); - nullable.tests(); - option.tests(); - quotation.tests(); - record.tests(); - regex.tests(); - resize_array.tests(); - result.tests(); - rune.tests(); - seq.tests(); - seq_expression.tests(); - set_.tests(); - string.tests(); - sudoku.tests(); - tailcall.tests(); - timespan.tests(); - tuple.tests(); - type_.tests(); - union.tests(); -} diff --git a/tests/Dart/src/AsyncTests.fs b/tests/Dart/src/AsyncTests.fs index 111dc43a0e..368aeec5a4 100644 --- a/tests/Dart/src/AsyncTests.fs +++ b/tests/Dart/src/AsyncTests.fs @@ -83,12 +83,14 @@ let tests () = f true + f false |> equal 22 +#if FABLE_COMPILER testCase "Non captured exception in async is propagated when using Async.StartImmediate" <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.StartImmediate) // Behaviour of Async.Start is the same as Async.StartImmediate in Dart testCase "Non captured exception in async is propagated when using Async.Start" <| fun () -> throwsAnyError (fun () -> async { failwith "boom!" } |> Async.Start) +#endif testCase "Simple async is executed correctly" <| fun () -> @@ -327,6 +329,7 @@ let tests () = equal 3 x } +#if FABLE_COMPILER testCaseAsync "Interaction between Async and Future works" <| fun () -> async { @@ -380,6 +383,7 @@ let tests () = equal "boom" message } +#endif testCase "Async try .. with returns correctly from 'with' branch" <| fun () -> diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index 47da2d9ccf..cb8aec2cdd 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -1,10 +1,12 @@ net10.0 + Exe Major + @@ -42,6 +44,6 @@ - + diff --git a/tests/Dart/src/Main.fs b/tests/Dart/src/Main.fs new file mode 100644 index 0000000000..df367d8380 --- /dev/null +++ b/tests/Dart/src/Main.fs @@ -0,0 +1,54 @@ +module Fable.Tests.Dart.Main + +let allTests = + [| + "Arithmetic", Arithmetic.tests + "Async", Async.tests + "Random", RandomTests.tests + "Array", Array.tests + "Comparison", Comparison.tests + "CustomOperator", CustomOperator.tests + "DateTime", DateTime.tests + "Dictionary", Dictionary.tests + "Enum", Enum.tests + "Enumerable", Enumerable.tests + "HashSet", HashSet.tests + "InlineIfLambda", InlineIfLambda.tests + "List", List.tests + "Map", Map.tests + "Misc", Misc.tests + "Nullable", Nullable.tests + "Option", Option.tests + "Quotation", Quotation.tests + "Record", Record.tests + "Regex", Regex.tests + "ResizeArray", ResizeArray.tests + "Result", Result.tests + "Rune", Rune.tests + "Seq", Seq.tests + "SeqExpression", SeqExpression.tests + "Set", Set.tests + "String", String.tests + "Sudoku", Sudoku.tests + "TailCall", TailCall.tests + "TimeSpan", TimeSpan.tests + "Tuple", Tuple.tests + "Type", Type.tests + "Union", Union.tests + |] + +#if FABLE_COMPILER +let main () = + for _, test in allTests do + test () + 0 +#else +open Expecto + +[] +let main args = + for name, test in allTests do + Util.collectExpectoModule name test + Util.getExpectoTests () + |> runTestsWithCLIArgs [] args +#endif \ No newline at end of file diff --git a/tests/Dart/src/QuotationTests.fs b/tests/Dart/src/QuotationTests.fs index cec666b682..80a93538a6 100644 --- a/tests/Dart/src/QuotationTests.fs +++ b/tests/Dart/src/QuotationTests.fs @@ -156,6 +156,7 @@ let tests() = let result = LeafExpressionConverter.EvaluateQuotation <@ (fun x -> (fun y -> x + y)) 3 4 @> equal 7 (result :?> int) +#if FABLE_COMPILER testCase "Evaluate tuple" <| fun () -> // NOTE: the canonical suite casts the result to `(int * int)`, but Dart // reifies generic type arguments, so `obj :?> (int * int)` fails for a @@ -165,6 +166,7 @@ let tests() = let arr = result :?> obj[] equal 1 (arr.[0] :?> int) equal 2 (arr.[1] :?> int) +#endif testCase "Expr.GetFreeVars returns empty for closed expr" <| fun () -> let q = <@ fun x -> x + 1 @> @@ -229,6 +231,7 @@ let tests() = | NewRecord(_ty, values) -> equal 1 (List.length values) | _ -> failwith "Expected NewRecord" +#if FABLE_COMPILER testCase "Evaluate mutable variable set (VarSet)" <| fun () -> let result = LeafExpressionConverter.EvaluateQuotation <@ let mutable v = 1 in (v <- 2); v @> equal 2 (result :?> int) @@ -239,6 +242,7 @@ let tests() = <@ let r = { Value = 1 }: MutableRec in r.Value <- 5; r.Value @> equal 5 (result :?> int) +#endif testCase "DerivedPatterns AndAlso, OrElse and SpecificCall work" <| fun () -> dpToSql <@ fun (c: DpRec) -> c.Country = "UK" @> |> equal "(col = ?)" diff --git a/tests/Dart/src/RuneTests.fs b/tests/Dart/src/RuneTests.fs index 9a307aebdc..4b2b967bd0 100644 --- a/tests/Dart/src/RuneTests.fs +++ b/tests/Dart/src/RuneTests.fs @@ -45,6 +45,7 @@ let tests () = Rune.GetRuneAt("ABC", 3) |> ignore +#if FABLE_COMPILER testCase "Rune.GetRuneAt throws for unpaired surrogate characters" <| fun () -> let highSurrogate = stringFromCharCode 0xD800 let lowSurrogate = stringFromCharCode 0xDC00 @@ -56,6 +57,7 @@ let tests () = throwsAnyError <| fun () -> Rune.GetRuneAt(lowSurrogate, 0) |> ignore +#endif testCase "Rune.Value returns the Unicode scalar value" <| fun () -> Rune.GetRuneAt("A", 0).Value @@ -106,6 +108,7 @@ let tests () = |> Seq.toList |> equal [ "A"; "𠀀"; "B" ] +#if FABLE_COMPILER testCase "String.EnumerateRunes replaces unpaired surrogates" <| fun () -> // .NET String.EnumerateRunes replaces invalid UTF-16 sequences with U+FFFD // https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.replacementchar @@ -118,3 +121,4 @@ let tests () = |> Seq.map _.Value |> Seq.toList |> equal [ 0xFFFD; 0x41; 0xFFFD ] +#endif diff --git a/tests/Dart/src/StringTests.fs b/tests/Dart/src/StringTests.fs index ced331096a..d3722dbd79 100644 --- a/tests/Dart/src/StringTests.fs +++ b/tests/Dart/src/StringTests.fs @@ -77,11 +77,13 @@ let tests() = s.[2..] |> equal "t and dog" s.[..8] |> equal "cat and d" +#if FABLE_COMPILER testCase "String literal addition is optimized" <| fun () -> "bar" + aLiteral |> equal "barfoo" "bar" + notALiteral |> equal "barfoo" obj.ReferenceEquals("bar" + aLiteral, "barfoo") |> equal true obj.ReferenceEquals("bar" + notALiteral, "barfoo") |> equal false +#endif testCase "String chunkBySize works" <| fun () -> // See #1296 "fffff" |> Seq.chunkBySize 3 |> Seq.map String |> Seq.toList diff --git a/tests/Dart/src/Tests.Util.fs b/tests/Dart/src/Tests.Util.fs index fad7e0d472..cb60e2c7a4 100644 --- a/tests/Dart/src/Tests.Util.fs +++ b/tests/Dart/src/Tests.Util.fs @@ -1,5 +1,7 @@ module Fable.Tests.Dart.Util +#if FABLE_COMPILER + open Fable.Core open Fable.Core.Dart @@ -22,6 +24,57 @@ let equal (expected: 'T) (actual: 'T) = Test.expect(actual, Test.equals(expected let notEqual (expected: 'T) (actual: 'T) = Test.expect(actual, Test.isNot(Test.equals(expected))) let throwsAnyError (f: unit -> 'a): unit = Test.expect(f, Test.throwsException) +#else + +open Expecto +let private expectoTestCase = testCase +let private expectoTestCaseAsync = testCaseAsync +let private expectoTestList = testList + +let mutable private registeredTests = [] +let mutable private registeredTestNames = Map.empty + +let private uniqueTestName (name: string) = + match Map.tryFind name registeredTestNames with + | Some count -> + registeredTestNames <- Map.add name (count + 1) registeredTestNames + $"{name} ({count + 1})" + | None -> + registeredTestNames <- Map.add name 1 registeredTestNames + name + +let private register test = + registeredTests <- test :: registeredTests + +let testCase (msg: string) (f: unit -> unit) = + register (expectoTestCase (uniqueTestName msg) f) + +let testCaseAsync (msg: string) (f: unit -> Async) = + register (expectoTestCaseAsync (uniqueTestName msg) (f ())) + +let testList (msg: string) (tests: unit list) = + let testCount = List.length tests + let childTests = registeredTests |> List.take testCount |> List.rev + registeredTests <- registeredTests |> List.skip testCount + register (expectoTestList (uniqueTestName msg) childTests) + +let getExpectoTests () = expectoTestList "All" (List.rev registeredTests) +let collectExpectoModule (name: string) (registerTests: unit -> unit) = + let previousTests = registeredTests + let previousNames = registeredTestNames + registeredTests <- [] + registeredTestNames <- Map.empty + registerTests () + let moduleTests = registeredTests |> List.rev + registeredTests <- expectoTestList name moduleTests :: previousTests + registeredTestNames <- previousNames + +let equal expected actual: unit = Expecto.Expect.equal actual expected "" +let notEqual expected actual: unit = Expecto.Expect.notEqual actual expected "" +let throwsAnyError (f: unit -> 'a): unit = Expecto.Expect.throws (fun () -> f () |> ignore) "" + +#endif + /// Matches the most specific type first, so an ArgumentOutOfRangeException is not reported as ArgumentException. let raisedExceptionType (f: unit -> 'a): string = try From f51333f48b8f7d05b15b9b50fd58fb9283cc04a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0=20=D0=A3=D0=BA=D1=80=D0=B0?= =?UTF-8?q?=D1=97=D0=BD=D1=96!=20=D0=93=D0=B5=D1=80=D0=BE=D1=8F=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=D0=B0!?= <777696+ncave@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:07:33 -0800 Subject: [PATCH 321/322] fix(rust): Wrap async test bodies with runSynchronously (#5289) --- src/Fable.Build/Test/Dart.fs | 1 + src/Fable.Transforms/Rust/Fable2Rust.fs | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Fable.Build/Test/Dart.fs b/src/Fable.Build/Test/Dart.fs index b4707b7976..fbc90c9b05 100644 --- a/src/Fable.Build/Test/Dart.fs +++ b/src/Fable.Build/Test/Dart.fs @@ -26,6 +26,7 @@ let handle (args: string list) = Shell.copyFile buildDir (testsFolder "analysis_options.yaml") Directory.GetFiles(testsFolder, "*.dart") |> Seq.iter (Shell.copyFile buildDir) + let testCmd = $"dart test {buildDir}/src/Main.dart" let fableArgs = diff --git a/src/Fable.Transforms/Rust/Fable2Rust.fs b/src/Fable.Transforms/Rust/Fable2Rust.fs index 887d72d73f..fa198834dd 100644 --- a/src/Fable.Transforms/Rust/Fable2Rust.fs +++ b/src/Fable.Transforms/Rust/Fable2Rust.fs @@ -4922,19 +4922,37 @@ module Util = let isByRefPreferred = memb.Attributes |> Seq.exists (fun a -> a.Entity.FullName = Atts.rustByRef) + let hasFactAttribute = + memb.Attributes + |> Seq.exists (fun a -> a.Entity.FullName.EndsWith(".FactAttribute", StringComparison.Ordinal)) + + let asyncTestReturnType = + match hasFactAttribute, decl.Body.Type with + | true, Replacements.Util.IsEntity (Types.fsharpAsyncGeneric) (_, [ returnType ]) -> Some returnType + | _ -> None + + let returnType = asyncTestReturnType |> Option.defaultValue decl.Body.Type // memb.ReturnParameter.Type + let fnDecl, fnBody, genArgs = let ctx = { ctx with IsParamByRefPreferred = isByRefPreferred } let parameters = memb.CurriedParameterGroups |> List.concat - let returnType = decl.Body.Type // memb.ReturnParameter.Type transformFunc com ctx parameters returnType (Some memb.FullName) decl.Args decl.Body + let fnBody = + match asyncTestReturnType with + | Some _returnType -> + // wrap the async test body with runSynchronously + let none = mkGenericPathExpr [ rawIdent "None" ] None + makeLibCall com ctx None "Async" "runSynchronously" [ fnBody; none; none ] + | None -> fnBody + let isUnsafe = memb.Attributes |> Seq.exists (fun a -> a.Entity.FullName = Atts.rustUnsafe) let fnBodyBlock = if isUnsafe then fnBody |> mkUnsafeBlockExpr |> mkBodyBlock - else if decl.Body.Type = Fable.Unit then + else if returnType = Fable.Unit then mkSemiBlock fnBody else mkExprBlock fnBody From f9657ad304803ff00c9a2ada398b5f586ad74e53 Mon Sep 17 00:00:00 2001 From: Maxime Mangel Date: Wed, 11 Nov 2026 18:47:58 +0100 Subject: [PATCH 322/322] fix(all): copy mutable struct records by value (#5270) --- src/Fable.Transforms/FSharp2Fable.Util.fs | 3 + src/Fable.Transforms/FSharp2Fable.fs | 54 +++++++++++++- tests/Beam/RecordTests.fs | 87 +++++++++++++++++++++++ tests/Dart/src/RecordTests.fs | 37 ++++++++++ tests/Js/Main/RecordTypeTests.fs | 80 +++++++++++++++++++++ tests/Python/TestRecordType.fs | 87 +++++++++++++++++++++++ tests/Rust/tests/src/RecordTests.fs | 13 ++++ 7 files changed, 359 insertions(+), 2 deletions(-) diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index b9ff3043a9..6eb9b17333 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -850,6 +850,9 @@ module Helpers = // typ.TypeDefinition.DisplayName = "inref" || // typ.TypeDefinition.DisplayName = "outref") + let isInRefType (typ: FSharpType) = + typ.HasTypeDefinition && typ.TypeDefinition.DisplayName = "inref" + let isByRefValue (value: FSharpMemberOrFunctionOrValue) = // Value type "this" is passed as inref, so it has to be excluded // (Note: the non-abbreviated type of inref and outref is byref) diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index ee731140ff..1ff1c704cf 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -771,6 +771,26 @@ let private withDefaultValueFields (com: IFableCompiler) (ctx: Context) (fsType: else args +let private derefByRefCallee + (com: IFableCompiler) + (ctx: Context) + r + (callee: FSharpExpr) + (calleeType: FSharpType) + (transformedCallee: Fable.Expr) + = + match callee with + | FSharpExprPatterns.Value var when + com.Options.Language <> Rust + && isByRefType callee.Type + && isByRefValue var + // An inref argument is passed as the value itself when its source is immutable + && not (isInRefType var.FullType) + && not (var.IsCompilerGenerated && var.CompiledName = "inputRecord") + -> + Replacements.Api.getRefCell com r (makeType ctx.GenericArgs calleeType) transformedCallee + | _ -> transformedCallee + let rec private transformDecisionTargets (com: IFableCompiler) (ctx: Context) @@ -1230,6 +1250,17 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs return (expr, bindings) ||> List.fold (fun e (i, v) -> Fable.Let(i, v, e)) + | FSharpExprPatterns.AddressOf inner when + var.IsCompilerGenerated + && var.CompiledName = "inputRecord" + && com.Options.Language <> Rust + -> + let! value = transformExpr com ctx [] inner + let ctx, ident = putIdentInScope com ctx var (Some value) + let ident = { ident with Type = value.Type } + let! body = transformExpr com ctx [] body + return Fable.Let(ident, value, body) + | _ when isInline var -> let ctx = { ctx with ScopeInlineValues = (var, value) :: ctx.ScopeInlineValues } @@ -1532,7 +1563,14 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs // Getters and Setters | FSharpExprPatterns.AnonRecordGet(callee, calleeType, fieldIndex) -> let r = makeRangeFrom fsExpr - let! callee = transformExpr com ctx [] callee + + let callee = + match callee with + | IgnoreAddressOf inner when com.Options.Language <> Rust -> inner + | _ -> callee + + let! transformedCallee = transformExpr com ctx [] callee + let callee = derefByRefCallee com ctx r callee calleeType transformedCallee let fieldName = calleeType.AnonRecordTypeDetails.SortedFieldNames[fieldIndex] @@ -1544,7 +1582,19 @@ let private transformExpr (com: IFableCompiler) (ctx: Context) appliedGenArgs fs | FSharpExprPatterns.FSharpFieldGet(callee, calleeType, field) -> let r = makeRangeFrom fsExpr - let! callee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some(IgnoreAddressOf inner) when com.Options.Language <> Rust -> Some inner + | callee -> callee + + let! transformedCallee = transformCallee com ctx callee calleeType + + let callee = + match callee with + | Some callee -> derefByRefCallee com ctx r callee calleeType transformedCallee + | None -> transformedCallee + // let typ = makeType ctx.GenericArgs fsExpr.Type // Doesn't always work let typ = resolveFieldType ctx calleeType field.FieldType diff --git a/tests/Beam/RecordTests.fs b/tests/Beam/RecordTests.fs index 8d13df6530..8c408a97fd 100644 --- a/tests/Beam/RecordTests.fs +++ b/tests/Beam/RecordTests.fs @@ -48,6 +48,38 @@ type IntegrityRecord = override this.ToString() = this.Level +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + [] let ``test simple record creation works`` () = let p = { Name = "Alice"; Age = 30 } @@ -311,3 +343,58 @@ let ``test Records with ref-type interior mutability`` () = x.MutRefValue |> equal "ab" x.MutRefValue <- x.MutRefValue + "c" x.MutRefValue |> equal "abc" + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Dart/src/RecordTests.fs b/tests/Dart/src/RecordTests.fs index 492b44ec17..04d12ebe71 100644 --- a/tests/Dart/src/RecordTests.fs +++ b/tests/Dart/src/RecordTests.fs @@ -58,6 +58,14 @@ type RecordShadowingFileScope = member this.Wrap() = { Value = this.Wrapped } member this.Add() = addOne this.addOne +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests() = testCase "Anonymous records work" <| fun () -> let r = makeAnonRec() @@ -181,3 +189,32 @@ let tests() = let record = { Wrapped = 3; addOne = 7 } :> IWrap record.Wrap().Value |> equal 3 record.Add() |> equal 8 + + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) diff --git a/tests/Js/Main/RecordTypeTests.fs b/tests/Js/Main/RecordTypeTests.fs index daabe90152..6b71bde751 100644 --- a/tests/Js/Main/RecordTypeTests.fs +++ b/tests/Js/Main/RecordTypeTests.fs @@ -70,6 +70,38 @@ let holdStructRecord (data: StructRecord) (path: int list) = Data = { data with Path = List.rev path } } +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + let tests = testList "RecordTypes" [ @@ -210,4 +242,52 @@ let tests = equal 1 a.xs.Count equal 1 b.xs.Count + testCase "Copy-and-update of a let mutable struct record keeps the unchanged fields" <| fun () -> + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + + testCase "Copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields" <| fun () -> + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + + testCase "Copy-and-update of a module-level mutable struct record keeps the unchanged fields" <| fun () -> + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + + testCase "Field reads and copy-and-update through a byref struct record parameter work" <| fun () -> + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + + testCase "Field reads through a byref or outref to a reference type work" <| fun () -> + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + + testCase "Copy-and-update of a let mutable anonymous struct record keeps the unchanged fields" <| fun () -> + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + + testCase "Field reads through a byref to an anonymous struct record work" <| fun () -> + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 + ] diff --git a/tests/Python/TestRecordType.fs b/tests/Python/TestRecordType.fs index 8d9ef01395..08a158aafb 100644 --- a/tests/Python/TestRecordType.fs +++ b/tests/Python/TestRecordType.fs @@ -61,6 +61,38 @@ type StructRecord = { Size: int; Seed: int; Path: int list } type StructRecordHolder = { Tag: int; Data: StructRecord } +[] +type StructRecordCopied = { T: float; S: string } + +[] +type StructRecordWithMutableField = { mutable MutableN: int; FixedN: int } + +[] +type StructRecordOrdered = { OrdA: int; OrdB: int; OrdC: int } + +type ByRefClassRecord = { BcP: int; BcQ: int } + +type ByRefClass = + val mutable BkF: int + val BkV: int + new() = { BkF = 0; BkV = 3 } + +let sumStructRecordByRef (r: byref) = r.OrdA + r.OrdC + +let copyStructRecordByRef (r: byref) = { r with OrdB = 99 } + +let sumClassRecordByRef (r: byref) = r.BcP + r.BcQ + +let setClassRecordOutRef (r: outref) = + r <- { BcP = 4; BcQ = 1 } + r.BcP + r.BcQ + +let sumClassByRef (r: byref) = r.BkF + r.BkV + +let sumAnonymousStructRecordByRef (x: byref) = x.AnX + x.AnY + +let mutable moduleStructRecord = { T = 1.0; S = "a" } + [] let ``test sprintf formats record fields with their F# names`` () = sprintf "%A" { Size = 1; Seed = 2; Path = [] } @@ -258,3 +290,58 @@ let ``test Copy-and-update of a struct record argument works`` () = equal 1 held.Data.Size equal 2 held.Data.Seed equal [ 8; 7 ] held.Data.Path + +[] +let ``test copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + +[] +let ``test copy-and-update of a let mutable struct record with mutable fields keeps the unchanged fields`` () = + let mutable p = { MutableN = 1; FixedN = 2 } + p.MutableN <- 5 + let q = { p with FixedN = 9 } + q.MutableN |> equal 5 + q.FixedN |> equal 9 + p.FixedN |> equal 2 + +[] +let ``test copy-and-update of a module-level mutable struct record keeps the unchanged fields`` () = + moduleStructRecord <- { moduleStructRecord with T = 7.0 } + let copy = { moduleStructRecord with S = "b" } + copy.T |> equal 7.0 + copy.S |> equal "b" + moduleStructRecord.S |> equal "a" + +[] +let ``test field reads and copy-and-update through a byref struct record parameter work`` () = + let mutable r = { OrdA = 1; OrdB = 2; OrdC = 3 } + sumStructRecordByRef &r |> equal 4 + let x = copyStructRecordByRef &r + (x.OrdA, x.OrdB, x.OrdC) |> equal (1, 99, 3) + +[] +let ``test field reads through a byref or outref to a reference type work`` () = + let mutable c = { BcP = 1; BcQ = 2 } + sumClassRecordByRef &c |> equal 3 + let mutable o = { BcP = 0; BcQ = 0 } + setClassRecordOutRef &o |> equal 5 + let mutable k = ByRefClass() + k.BkF <- 4 + sumClassByRef &k |> equal 7 + +[] +let ``test copy-and-update of a let mutable anonymous struct record keeps the unchanged fields`` () = + let mutable a = struct {| X = 1; Y = 2 |} + let b = {| a with Y = 99 |} + (b.X, b.Y) |> equal (1, 99) + +[] +let ``test field reads through a byref to an anonymous struct record work`` () = + let mutable a = struct {| AnX = 1; AnY = 2 |} + sumAnonymousStructRecordByRef &a |> equal 3 diff --git a/tests/Rust/tests/src/RecordTests.fs b/tests/Rust/tests/src/RecordTests.fs index 4913a83613..422f3238b5 100644 --- a/tests/Rust/tests/src/RecordTests.fs +++ b/tests/Rust/tests/src/RecordTests.fs @@ -11,6 +11,19 @@ type Scene = { let getCamera (scene: Scene) = scene.Camera +[] +type StructRecordCopied = { T: float; S: string } + +[] +let ``Copy-and-update of a let mutable struct record keeps the unchanged fields`` () = + let mutable mark = { T = 1.0; S = "a" } + let copies = ResizeArray() + for i in 1..2 do + mark <- { T = float i; S = "b" } + copies.Add { mark with S = "c" } + copies |> Seq.map (fun m -> m.T) |> List.ofSeq |> equal [ 1.0; 2.0 ] + copies |> Seq.map (fun m -> m.S) |> List.ofSeq |> equal [ "c"; "c" ] + [] let ``Class-valued record fields compile`` () = let scene = { Camera = Camera() }