@@ -711,6 +711,105 @@ extension DSLTree.Node {
711711 }
712712}
713713
714+ extension DSLTree . Node {
715+ /// Implementation for `canOnlyMatchAtStart`, which maintains the option
716+ /// state.
717+ ///
718+ /// For a given specific node, this method can return one of three values:
719+ ///
720+ /// - `true`: This node is guaranteed to match only at the start of a subject.
721+ /// - `false`: This node can match anywhere in the subject.
722+ /// - `nil`: This node is inconclusive about where it can match.
723+ ///
724+ /// In particular, non-required groups and option-setting groups are
725+ /// inconclusive about where they can match.
726+ private func _canOnlyMatchAtStartImpl( _ options: inout MatchingOptions ) -> Bool ? {
727+ switch self {
728+ // Defining cases
729+ case . atom( . assertion( . startOfSubject) ) :
730+ return true
731+ case . atom( . assertion( . caretAnchor) ) :
732+ return !options. anchorsMatchNewlines
733+
734+ // Changing options doesn't determine `true`/`false`.
735+ case . atom( . changeMatchingOptions( let sequence) ) :
736+ options. apply ( sequence. ast)
737+ return nil
738+
739+ // Any other atom or consuming node returns `false`.
740+ case . atom, . customCharacterClass, . quotedLiteral:
741+ return false
742+
743+ // Trivia/empty have no effect.
744+ case . trivia, . empty:
745+ return nil
746+
747+ // In an alternation, all of its children must match only at start.
748+ case . orderedChoice( let children) :
749+ return children. allSatisfy { $0. _canOnlyMatchAtStartImpl ( & options) == true }
750+
751+ // In a concatenation, the first definitive child provides the answer.
752+ case . concatenation( let children) :
753+ for child in children {
754+ if let result = child. _canOnlyMatchAtStartImpl ( & options) {
755+ return result
756+ }
757+ }
758+ return false
759+
760+ // Groups (and other parent nodes) defer to the child.
761+ case . nonCapturingGroup( let kind, let child) :
762+ options. beginScope ( )
763+ defer { options. endScope ( ) }
764+ if case . changeMatchingOptions( let sequence) = kind. ast {
765+ options. apply ( sequence)
766+ }
767+ return child. _canOnlyMatchAtStartImpl ( & options)
768+ case . capture( _, _, let child, _) :
769+ options. beginScope ( )
770+ defer { options. endScope ( ) }
771+ return child. _canOnlyMatchAtStartImpl ( & options)
772+ case . ignoreCapturesInTypedOutput( let child) ,
773+ . convertedRegexLiteral( let child, _) :
774+ return child. _canOnlyMatchAtStartImpl ( & options)
775+
776+ // A quantification that doesn't require its child to exist can still
777+ // allow a start-only match. (e.g. `/(foo)?^bar/`)
778+ case . quantification( let amount, _, let child) :
779+ return amount. requiresAtLeastOne
780+ ? child. _canOnlyMatchAtStartImpl ( & options)
781+ : nil
782+
783+ // For conditional nodes, both sides must require matching at start.
784+ case . conditional( _, let child1, let child2) :
785+ return child1. _canOnlyMatchAtStartImpl ( & options) == true
786+ && child2. _canOnlyMatchAtStartImpl ( & options) == true
787+
788+ // Extended behavior isn't known, so we return `false` for safety.
789+ case . consumer, . matcher, . characterPredicate, . absentFunction:
790+ return false
791+ }
792+ }
793+
794+ /// Returns a Boolean value indicating whether the regex with this node as
795+ /// the root can _only_ match at the start of a subject.
796+ ///
797+ /// For example, these regexes can only match at the start of a subject:
798+ ///
799+ /// - `/^foo/`
800+ /// - `/(^foo|^bar)/` (both sides of the alternation start with `^`)
801+ ///
802+ /// These can match other places in a subject:
803+ ///
804+ /// - `/(^foo)?bar/` (`^` is in an optional group)
805+ /// - `/(^foo|bar)/` (only one side of the alternation starts with `^`)
806+ /// - `/(?m)^foo/` (`^` means "the start of a line" due to `(?m)`)
807+ internal func canOnlyMatchAtStart( ) -> Bool {
808+ var options = MatchingOptions ( )
809+ return _canOnlyMatchAtStartImpl ( & options) ?? false
810+ }
811+ }
812+
714813// MARK: AST wrapper types
715814//
716815// These wrapper types are required because even @_spi-marked public APIs can't
@@ -818,6 +917,17 @@ extension DSLTree {
818917 public static func range( _ lower: Int , _ upper: Int ) -> Self {
819918 . init( ast: . range( . init( lower, at: . fake) , . init( upper, at: . fake) ) )
820919 }
920+
921+ internal var requiresAtLeastOne : Bool {
922+ switch ast {
923+ case . zeroOrOne, . zeroOrMore, . upToN:
924+ return false
925+ case . oneOrMore:
926+ return true
927+ case . exactly( let num) , . nOrMore( let num) , . range( let num, _) :
928+ return num. value. map { $0 > 0 } ?? false
929+ }
930+ }
821931 }
822932
823933 @_spi ( RegexBuilder)
0 commit comments