diff --git a/pyrightconfig.ci.json b/pyrightconfig.ci.json index 040f1551d..08b7c4fa0 100644 --- a/pyrightconfig.ci.json +++ b/pyrightconfig.ci.json @@ -4,6 +4,8 @@ "**/.venv/**", "**/node_modules/**", "temp/tests/Python/test_applicative.py", + "temp/tests/Python/test_hash_set.py", + "temp/tests/Python/test_nested_and_recursive_pattern.py", "temp/tests/Python/fable_modules/thoth_json_python/encode.py" ] } diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 0d1733455..bbc50feb3 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [Python] Fix HashSet operations (Count, Contains, Remove, UnionWith, IntersectWith, ExceptWith) to work with both native Python sets and custom MutableSet (by @dbrattli) * [Python] Fix `Array.length`, `.Length`, `Array.isEmpty`, and `ResizeArray.Count` to use `len()` instead of `.length` property for plain Python list interop (by @dbrattli) * [Python] Fix `Task` pass-through returns not being awaited in if/else and try/with branches (by @dbrattli) * [Python] Fix `:? T as x` type test pattern in closures causing `UnboundLocalError` due to `cast()` shadowing outer variable (by @dbrattli) diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 32a5c3c0b..dd37918af 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* [Python] Fix HashSet operations (Count, Contains, Remove, UnionWith, IntersectWith, ExceptWith) to work with both native Python sets and custom MutableSet (by @dbrattli) * [Python] Fix `Array.length`, `.Length`, `Array.isEmpty`, and `ResizeArray.Count` to use `len()` instead of `.length` property for plain Python list interop (by @dbrattli) * [Python] Fix `Task` pass-through returns not being awaited in if/else and try/with branches (by @dbrattli) * [Python] Fix `:? T as x` type test pattern in closures causing `UnboundLocalError` due to `cast()` shadowing outer variable (by @dbrattli) diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 479ecb76d..46e242afa 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -3679,8 +3679,7 @@ let declareType let body, stmts = transformReflectionInfo com ctx None ent generics let expr, stmts' = makeFunctionExpression com ctx None (args, body, [], ta) - let name = - com.GetIdentifier(ctx, Naming.toPascalCase entName + Naming.reflectionSuffix) + let name = com.GetIdentifier(ctx, entName + Naming.reflectionSuffix) expr |> declareModuleMember com ctx ent.IsPublic name None, stmts @ stmts' @@ -4065,8 +4064,7 @@ let transformUnion (com: IPythonCompiler) ctx (ent: Fable.Entity) (entName: stri let body, stmts = transformReflectionInfo com ctx None ent generics let expr, stmts' = makeFunctionExpression com ctx None (args, body, [], ta) - let name = - com.GetIdentifier(ctx, Naming.toPascalCase entName + Naming.reflectionSuffix) + let name = com.GetIdentifier(ctx, entName + Naming.reflectionSuffix) expr |> declareModuleMember com ctx ent.IsPublic name None, stmts @ stmts' diff --git a/src/Fable.Transforms/Python/Replacements.fs b/src/Fable.Transforms/Python/Replacements.fs index b23b8ac78..3422c61d2 100644 --- a/src/Fable.Transforms/Python/Replacements.fs +++ b/src/Fable.Transforms/Python/Replacements.fs @@ -2630,17 +2630,25 @@ let hashSets (com: ICompiler) (ctx: Context) r t (i: CallInfo) (thisArg: Expr op |> makeHashSetWithComparer com r t (makeArray Any []) |> Some | _ -> None - | "get_Count", Some c, _ -> - Helper.LibCall(com, "mutable_set", "HashSet__get_Count", t, [ c ], ?loc = r) - |> Some + | "get_Count", Some c, _ -> Helper.GlobalCall("len", t, [ c ], ?loc = r) |> Some | "get_IsReadOnly", _, _ -> BoolConstant false |> makeValue r |> Some - | ReplaceName [ "Clear", "clear"; "Contains", "has"; "Remove", "delete" ] methName, Some c, args -> - Helper.InstanceCall(c, methName, t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "Clear", Some c, _ -> Helper.InstanceCall(c, "clear", t, args, i.SignatureArgTypes, ?loc = r) |> Some + | "Contains", Some c, [ arg ] -> + Helper.LibCall(com, "map_util", "containsInSet", t, [ arg; c ], ?loc = r) + |> Some + | "Remove", Some c, [ arg ] -> + Helper.LibCall(com, "map_util", "removeFromSet", t, [ arg; c ], ?loc = r) + |> Some | "GetEnumerator", Some c, _ -> getEnumerator com r t c |> Some | "Add", Some c, [ arg ] -> Helper.LibCall(com, "map_util", "addToSet", t, [ arg; c ], ?loc = r) |> Some - | ("IsProperSubsetOf" | "IsProperSupersetOf" | "UnionWith" | "IntersectWith" | "ExceptWith" | "IsSubsetOf" | "IsSupersetOf" as meth), - Some c, - args -> + | "UnionWith", Some c, [ arg ] -> Helper.LibCall(com, "map_util", "unionWithSet", t, [ c; arg ], ?loc = r) |> Some + | "IntersectWith", Some c, [ arg ] -> + Helper.LibCall(com, "map_util", "intersectWithSet", t, [ c; arg ], ?loc = r) + |> Some + | "ExceptWith", Some c, [ arg ] -> + Helper.LibCall(com, "map_util", "exceptWithSet", t, [ c; arg ], ?loc = r) + |> Some + | ("IsProperSubsetOf" | "IsProperSupersetOf" | "IsSubsetOf" | "IsSupersetOf" as meth), Some c, args -> let meth = Naming.lowerFirst meth let args = injectArg com ctx r "Set" meth i.GenericArgs args diff --git a/src/fable-library-py/fable_library/map_util.py b/src/fable-library-py/fable_library/map_util.py index e309d70ec..8bcc655db 100644 --- a/src/fable-library-py/fable_library/map_util.py +++ b/src/fable-library-py/fable_library/map_util.py @@ -64,6 +64,40 @@ def add_to_set(v: object, st: MutableSet[object]) -> bool: return True +def remove_from_set(v: object, st: MutableSet[object]) -> bool: + """Remove from set-like object - returns True if removed, False if not present.""" + if v in st: + st.discard(v) + return True + + return False + + +def contains_in_set(v: object, st: MutableSet[object]) -> bool: + """Check if set-like object contains a value.""" + return v in st + + +def union_with_set(s1: MutableSet[object], s2: Iterable[object]) -> None: + """Add all elements from s2 to s1. Works with both native set and custom HashSet.""" + for x in s2: + s1.add(x) + + +def intersect_with_set(s1: MutableSet[object], s2: Iterable[object]) -> None: + """Remove elements from s1 that are not in s2. Works with both native set and custom HashSet.""" + s2_items = set(s2) + to_remove = [x for x in s1 if x not in s2_items] + for x in to_remove: + s1.discard(x) + + +def except_with_set(s1: MutableSet[object], s2: Iterable[object]) -> None: + """Remove all elements in s2 from s1. Works with both native set and custom HashSet.""" + for x in s2: + s1.discard(x) + + def add_to_dict(di: dict[_K, _V], k: _K, v: _V) -> None: if k in di: raise Exception("An item with the same key has already been added. Key: " + str(k)) diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e25ffbc46..cb13d266a 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -77,6 +77,8 @@ + + diff --git a/tests/Python/TestHashSet.fs b/tests/Python/TestHashSet.fs new file mode 100644 index 000000000..945f460ed --- /dev/null +++ b/tests/Python/TestHashSet.fs @@ -0,0 +1,271 @@ +module Fable.Tests.HashSet + +open System.Collections.Generic +open Util.Testing + +type MyRefType(i: int) = + member x.Value = i + +let inline set l = + let xs = HashSet<_>() + for x in l do + xs.Add x |> ignore + xs + +type MyRecord = { a: int } + +type R = { i: int; s: string } + +type Apa<'t when 't : equality>() = + let state = HashSet<'t>() + member _.Add t = state.Add t |> ignore + member _.Contains t = state.Contains t + +[] +let ``test HashSet ctor from Enumerable works`` () = + let s = List.toSeq [1;2;2;3] + let xs = HashSet(s) + xs |> Seq.toList + |> equal [1;2;3] + +[] +let ``test HashSet ctor creates empty HashSet`` () = + let xs = HashSet() + xs |> Seq.isEmpty + |> equal true + +[] +let ``test HashSets with IEqualityComparer work`` () = + let x = MyRefType(4) + let y = MyRefType(4) + let z = MyRefType(6) + let set = HashSet<_>() + set.Add(x) |> equal true + set.Contains(x) |> equal true + set.Contains(y) |> equal false + + let comparer = + { new IEqualityComparer with + member _.Equals(x, y) = x.Value = y.Value + member _.GetHashCode(x) = x.Value } + let set2 = HashSet<_>(comparer) + set2.Add(x) |> equal true + set2.Contains(x) |> equal true + set2.Contains(y) |> equal true + set2.Contains(z) |> equal false + +[] +let ``test HashSet.Add returns true if not present`` () = + let xs = set [] + xs.Add(1) |> equal true + xs.Count |> equal 1 + +[] +let ``test HashSet.Add returns false if already present`` () = + let xs = set [1] + xs.Add(1) |> equal false + xs.Count |> equal 1 + +[] +let ``test HashSet.Remove works when item is present`` () = + let xs = set [1] + xs.Remove 1 |> equal true + xs.Count |> equal 0 + +[] +let ``test HashSet.Remove works when item is not present`` () = + let xs = set [1; 2] + xs.Remove 3 |> equal false + xs.Count |> equal 2 + +[] +let ``test HashSet.UnionWith works`` () = + let xs = set [1; 2] + let ys = set [2; 4] + xs.UnionWith ys + (xs.Contains 1 && xs.Contains 2 && xs.Contains 4) + |> equal true + +[] +let ``test HashSet.IntersectWith works`` () = + let xs = set [1; 2] + let ys = set [2; 4] + xs.IntersectWith ys + xs.Contains 1 |> equal false + xs.Contains 2 |> equal true + +// TODO: IntersectWith with custom comparison needs IEqualityComparer +// support in intersect_with_set helper +// [] +// let ``test IntersectWith works with custom comparison`` () = // See #2566 +// let ignoreCase = +// { new IEqualityComparer with +// member _.Equals(s1: string, s2: string) = +// s1.Equals(s2, System.StringComparison.InvariantCultureIgnoreCase) +// member _.GetHashCode(s: string) = s.ToLowerInvariant().GetHashCode() } +// let set = new HashSet(["Foo"; "bar"], ignoreCase) +// set.Contains("foo") |> equal true +// set.Contains("Foo") |> equal true +// set.Contains("bar") |> equal true +// set.Contains("Bar") |> equal true +// set.IntersectWith(["foo"; "bar"]) +// set.Count |> equal 2 +// set.IntersectWith(["Foo"; "Bar"]) +// set.Count |> equal 2 + +[] +let ``test HashSet.ExceptWith works`` () = + let xs = set [1; 2] + let ys = set [2; 4] + xs.ExceptWith ys + xs.Contains 1 |> equal true + xs.Contains 2 |> equal false + +[] +let ``test HashSet creation works`` () = + let hs = HashSet<_>() + equal 0 hs.Count + +[] +let ``test HashSet iteration works`` () = + let hs = HashSet<_>() + for i in 1. .. 10. do hs.Add(i*i) |> ignore + + let i = ref 0. + for v in hs do + i := v + !i + equal 385. !i + +[] +let ``test HashSet folding works`` () = + let hs = HashSet<_>() + for i in 1. .. 10. do hs.Add(i*i) |> ignore + hs |> Seq.fold (fun acc item -> acc + item) 0. + |> equal 385. + +[] +let ``test HashSet.Count works`` () = + let hs = HashSet<_>() + for i in 1. .. 10. do hs.Add(i*i) |> ignore + hs.Count + |> equal 10 + let xs = set [] + xs.Count |> equal 0 + let ys = set [1] + ys.Count |> equal 1 + let zs = set [1; 1] + zs.Count |> equal 1 + let zs' = set [1; 2] + zs'.Count |> equal 2 + +[] +let ``test HashSet.Add works`` () = + let hs = HashSet<_>() + hs.Add("A") |> equal true + hs.Add("B") |> equal true + hs.Count |> equal 2 + +[] +let ``test HashSet.Add with records works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let hs = HashSet<_>() + hs.Add(x1) |> equal true + hs.Add(x2) |> equal false + hs.Add(x3) |> equal true + hs.Count |> equal 2 + +[] +let ``test HashSet.Clear works`` () = + let hs = HashSet<_>() + hs.Add(1) |> equal true + hs.Add(2) |> equal true + hs.Clear() + hs.Count |> equal 0 + +[] +let ``test HashSet.Contains works`` () = + let hs = HashSet<_>() + hs.Add("Hello") |> equal true + hs.Add("World!") |> equal true + hs.Contains("Hello") |> equal true + hs.Contains("Everybody!") |> equal false + +[] +let ``test HashSet.Remove works`` () = + let hs = HashSet<_>() + hs.Add("A") |> ignore + hs.Add("B") |> ignore + hs.Remove("A") |> equal true + hs.Remove("C") |> equal false + +[] +let ``test HashSet.Remove with records works`` () = + let x1 = { a = 5 } + let x2 = { a = 5 } + let x3 = { a = 10 } + let hs = HashSet<_>() + hs.Add(x1) |> ignore + hs.Add(x2) |> ignore + hs.Count |> equal 1 + hs.Remove(x3) |> equal false + hs.Count |> equal 1 + hs.Remove(x1) |> equal true + hs.Count |> equal 0 + +[] +let ``test HashSet equality works with generics`` () = // See #1712 + let apa = Apa() + apa.Add({ i = 5; s = "foo"}) + apa.Contains ({ i = 5; s = "foo"}) |> equal true + apa.Contains ({ i = 5; s = "fo"}) |> equal false + +// TODO: ICollection interface tests are disabled for Python because ICollection +// interface dispatch has a separate issue with comparer arguments. +// See: HashSet ICollection.Contains, ICollection.Remove, etc. + +// [] +// 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.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 + +// [] +// 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.Add works`` () = +// let xs = [| ("A", 1); ("B", 2); ("C", 3) |] +// let coll = (HashSet xs) :> ICollection<_> +// coll.Add(("A", 1)) +// coll.Add(("A", 2)) +// 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 diff --git a/tests/Python/TestNestedAndRecursivePattern.fs b/tests/Python/TestNestedAndRecursivePattern.fs new file mode 100644 index 000000000..7efd7f8d2 --- /dev/null +++ b/tests/Python/TestNestedAndRecursivePattern.fs @@ -0,0 +1,133 @@ +module Fable.Tests.NestedAndRecursivePattern + +open Util.Testing + +type Position (line: int32, col: int32) = + member p.Line = line + member p.Column = col + + +let relaxWhitespace2 = false + +type token = +| SIG | CLASS | STRUCT | INTERFACE | LBRACK_LESS | INTERP_STRING_BEGIN_PART | INTERP_STRING_PART +| BEGIN | LPAREN | LBRACE | LBRACE_BAR | LBRACK | LBRACK_BAR | LQUOTE | LESS + +type Context = + | CtxtLetDecl of bool * Position + | CtxtIf of Position + | CtxtTry of Position + | CtxtFun of Position + | CtxtFunction of Position + | CtxtWithAsLet of Position + | CtxtWithAsAugment of Position + | CtxtMatch of Position + | CtxtFor of Position + | CtxtWhile of Position + | CtxtWhen of Position + | CtxtVanilla of Position * bool + | CtxtThen of Position + | CtxtElse of Position + | CtxtDo of Position + | CtxtInterfaceHead of Position + | CtxtTypeDefns of Position + + | CtxtNamespaceHead of Position * token + | CtxtMemberHead of Position + | CtxtMemberBody of Position + | CtxtModuleBody of Position * bool + | CtxtNamespaceBody of Position + | CtxtException of Position + | CtxtParen of token * Position + | CtxtSeqBlock of FirstInSequence * Position * AddBlockEnd + | CtxtMatchClauses of bool * Position + + member c.StartPos = + match c with + | CtxtNamespaceHead (p, _) + | CtxtException p | CtxtModuleBody (p, _) | CtxtNamespaceBody p + | CtxtLetDecl (_, p) | CtxtDo p | CtxtInterfaceHead p | CtxtTypeDefns p | CtxtParen (_, p) | CtxtMemberHead p | CtxtMemberBody p + | CtxtWithAsLet p + | CtxtWithAsAugment p + | CtxtMatchClauses (_, p) | CtxtIf p | CtxtMatch p | CtxtFor p | CtxtWhile p | CtxtWhen p | CtxtFunction p | CtxtFun p | CtxtTry p | CtxtThen p | CtxtElse p | CtxtVanilla (p, _) + | CtxtSeqBlock (_, p, _) -> p + + member c.StartCol = c.StartPos.Column + +and AddBlockEnd = AddBlockEnd | NoAddBlockEnd | AddOneSidedBlockEnd +and FirstInSequence = FirstInSeqBlock | NotFirstInSeqBlock +and LexingModuleAttributes = LexingModuleAttributes | NotLexingModuleAttributes + +[] +type PositionWithColumn = + val Position: Position + val Column: int + new (position: Position, column: int) = { Position = position; Column = column } + override this.ToString() = $"L{this.Position.Line}-C1{this.Position.Column}-C2{this.Column}" + +let rec undentationLimit strict stack = + match stack with + | CtxtVanilla _ :: rest -> undentationLimit strict rest + + | CtxtSeqBlock _ :: rest when not strict -> undentationLimit strict rest + | CtxtParen _ :: rest when not strict -> undentationLimit strict rest + | (CtxtMatch _ as ctxt1) :: CtxtSeqBlock _ :: (CtxtParen ((BEGIN | LPAREN), _) as ctxt2) :: _ + -> if ctxt1.StartCol <= ctxt2.StartCol + then PositionWithColumn(ctxt1.StartPos, ctxt1.StartCol) + else PositionWithColumn(ctxt2.StartPos, ctxt2.StartCol) + | (CtxtMatchClauses _ as ctxt1) :: (CtxtMatch _) :: CtxtSeqBlock _ :: (CtxtParen ((BEGIN | LPAREN), _) as ctxt2) :: _ when relaxWhitespace2 + -> if ctxt1.StartCol <= ctxt2.StartCol + then PositionWithColumn(ctxt1.StartPos, ctxt1.StartCol) + else PositionWithColumn(ctxt2.StartPos, ctxt2.StartCol) + | CtxtFunction _ :: CtxtSeqBlock _ :: (CtxtLetDecl _ as limitCtxt) :: _rest + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | CtxtFunction _ :: rest + -> undentationLimit false rest + | (CtxtMatchClauses _ :: (CtxtTry _ as limitCtxt) :: _rest) + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | (CtxtMatchClauses _ :: (CtxtMatch _ as limitCtxt) :: _rest) when relaxWhitespace2 + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | CtxtFun _ :: rest + -> undentationLimit false rest + | CtxtParen ((LBRACE | LBRACK | LBRACK_BAR), _) :: CtxtSeqBlock _ :: rest + | CtxtParen ((LBRACE | LBRACK | LBRACK_BAR), _) :: CtxtVanilla _ :: CtxtSeqBlock _ :: rest + | CtxtSeqBlock _ :: CtxtParen((LBRACE | LBRACK | LBRACK_BAR), _) :: CtxtVanilla _ :: CtxtSeqBlock _ :: rest + -> undentationLimit false rest + | CtxtElse _ :: (CtxtIf _ as limitCtxt) :: _rest + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | (CtxtInterfaceHead _ | CtxtMemberHead _ | CtxtException _ | CtxtTypeDefns _ as limitCtxt :: _rest) + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + + | (CtxtWithAsAugment _ | CtxtThen _ | CtxtElse _ | CtxtDo _ ) :: rest + -> undentationLimit false rest + | CtxtParen ((SIG | STRUCT | BEGIN), _) :: CtxtSeqBlock _ :: (CtxtModuleBody (_, false) as limitCtxt) :: _ + | CtxtParen ((BEGIN | LPAREN | LBRACK | LBRACE | LBRACE_BAR | LBRACK_BAR), _) :: CtxtSeqBlock _ :: CtxtThen _ :: (CtxtIf _ as limitCtxt) :: _ + | CtxtParen ((BEGIN | LPAREN | LBRACK | LBRACE | LBRACE_BAR | LBRACK_BAR | LBRACK_LESS), _) :: CtxtSeqBlock _ :: CtxtElse _ :: (CtxtIf _ as limitCtxt) :: _ + | CtxtParen ((BEGIN | LPAREN | LESS | LBRACK | LBRACK_BAR), _) :: CtxtVanilla _ :: (CtxtSeqBlock _ as limitCtxt) :: _ + | CtxtParen ((CLASS | STRUCT | INTERFACE), _) :: CtxtSeqBlock _ :: (CtxtTypeDefns _ as limitCtxt) :: _ + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol + 1) + | CtxtSeqBlock _ :: CtxtParen(LPAREN, _) :: (CtxtTypeDefns _ as limitCtxt) :: _ + | CtxtSeqBlock _ :: CtxtParen(LPAREN, _) :: (CtxtMemberHead _ as limitCtxt) :: _ + | CtxtWithAsLet _ :: (CtxtMemberHead _ as limitCtxt) :: _ + when relaxWhitespace2 + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol + 1) + | CtxtSeqBlock _ :: CtxtParen((BEGIN | LPAREN | LBRACK | LBRACK_BAR), _) :: CtxtVanilla _ :: (CtxtSeqBlock _ as limitCtxt) :: _ + | CtxtParen ((BEGIN | LPAREN | LBRACE | LBRACE_BAR | LBRACK | LBRACK_BAR), _) :: CtxtSeqBlock _ :: (CtxtTypeDefns _ | CtxtLetDecl _ | CtxtMemberBody _ | CtxtWithAsLet _ as limitCtxt) :: _ + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol + 1) + | (CtxtIf _ as limitCtxt) :: _rest + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | (CtxtFor _ | CtxtWhile _ as limitCtxt) :: _rest + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + | (CtxtInterfaceHead _ | CtxtNamespaceHead _ + | CtxtException _ | CtxtModuleBody (_, false) | CtxtIf _ | CtxtWithAsLet _ | CtxtLetDecl _ | CtxtMemberHead _ | CtxtMemberBody _ as limitCtxt :: _) + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol + 1) + | (CtxtParen _ | CtxtFor _ | CtxtWhen _ | CtxtWhile _ | CtxtTypeDefns _ | CtxtMatch _ | CtxtModuleBody (_, true) | CtxtNamespaceBody _ | CtxtTry _ | CtxtMatchClauses _ | CtxtSeqBlock _ as limitCtxt :: _) + -> PositionWithColumn(limitCtxt.StartPos, limitCtxt.StartCol) + + | _ -> PositionWithColumn(Position(0, 0), 0) + +[] +let ``test Nested and Recursive Patterns works`` () = // See #3411 + let ctx1 = Position (5, 8) |> CtxtWhile + let ctx2 = Position (78, 2) |> CtxtWhile + undentationLimit true [ctx1; ctx2] |> string |> equal "L5-C18-C28"