1// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers 2// 3// SPDX-License-Identifier: MPL-2.0 4 5public struct Parser { 6 public struct MarkOpened { 7 internal let index: Int 8 } 9 10 let source: String 11 let tokens: [Token] 12 13 public init(source: String, tokens: [Token]) { 14 self.source = source 15 self.tokens = tokens 16 } 17 18 public private(set) var diagnostics: [Diagnostic] = [] 19 public var builder: SyntaxTreeBuilder = SyntaxTreeBuilder() 20 21 private var inError: Bool = false 22 private var position: Int = 0 23 private var absoluteUtf16Offset: Int = 0 24 public var absoluteRangeAtCursor: Range<Int> { 25 return absoluteUtf16Offset..<absoluteUtf16Offset 26 } 27 28 public var absoluteRangeOfCurrentToken: Range<Int> { 29 return absoluteUtf16Offset..<absoluteUtf16Offset + currentToken.utf16Length 30 } 31 32 public var isEndOfFile: Bool { 33 position == tokens.count 34 } 35 36 public var currentToken: Token { 37 if tokens.indices.contains(position) { 38 return tokens[position] 39 } else { 40 return Token( 41 kind: .eof, 42 text: "", 43 ) 44 } 45 } 46 47 public func isAt(kind: TokenKind) -> Bool { 48 currentToken.kind == kind 49 } 50 51 public func isAt(kindSatisfying predicate: (TokenKind) -> Bool) -> Bool { 52 return predicate(currentToken.kind) 53 } 54 55 56 public mutating func advance(metadata: TokenMetadata?) { 57 precondition(!isEndOfFile) 58 builder.advance(token: currentToken, metadata: metadata) 59 absoluteUtf16Offset += currentToken.utf16Length 60 position += 1 61 } 62 63 public mutating func advance(error: String, metadata: TokenMetadata? = nil) { 64 let mark = builder.open() 65 let diagnostic = Diagnostic( 66 message: error, 67 absoluteRange: absoluteRangeOfCurrentToken 68 ) 69 70 diagnostics.append(diagnostic) 71 advance(metadata: metadata) 72 builder.close(mark: mark, kind: .error, metadata: nil) 73 } 74 75 public mutating func eat(kind: TokenKind, metadata: TokenMetadata?) -> Bool { 76 guard !isEndOfFile && isAt(kindSatisfying: { $0 == kind }) else { return false } 77 advance(metadata: metadata) 78 return true 79 } 80 81 enum ControlFlow { 82 case `continue` 83 case `break` 84 } 85 86 mutating func ate(kind: TokenKind, metadata: TokenMetadata?) -> ControlFlow { 87 guard eat(kind: kind, metadata: metadata) else { return .continue } 88 inError = false 89 eatTrivia() 90 return .break 91 } 92 93 mutating func recoverUntil(_ anchors: Set<TokenKind>, expected: TokenKind, error: String? = nil) { 94 var discardTokens: [Token] = [] 95 let startOffset = absoluteUtf16Offset 96 97 while !self.isAt(kindSatisfying: { anchors.contains($0) }) { 98 if isEndOfFile { break } 99 let token = currentToken 100 advance(metadata: nil) 101 discardTokens.append(token) 102 } 103 104 var endOffset = startOffset 105 106 let error = error ?? "Expected \(expected) but got \(discardTokens)" 107 108 if discardTokens.isEmpty { 109 if !inError { 110 inError = true 111 diagnostics.append(Diagnostic(message: error, absoluteRange: absoluteRangeAtCursor)) 112 } 113 return 114 } else { 115 let mark = builder.open() 116 for discardToken in discardTokens { 117 endOffset += discardToken.utf16Length 118 } 119 120 builder.close(mark: mark, kind: .error, metadata: nil) 121 122 if !inError { 123 inError = true 124 diagnostics.append(Diagnostic(message: error, absoluteRange: startOffset..<endOffset)) 125 } 126 } 127 } 128 129 public mutating func expect(kind: TokenKind, metadata: TokenMetadata?, recovery: Set<TokenKind>, error: String? = nil) { 130 var anchors = recovery 131 if ate(kind: kind, metadata: metadata) == .break { return } 132 anchors.insert(kind) 133 recoverUntil(anchors, expected: kind, error: error) 134 let _ = ate(kind: kind, metadata: metadata) 135 } 136} 137 138extension Parser { 139 mutating func eatTrivium() -> Bool { 140 switch currentToken.kind { 141 case .whitespace, .newline: 142 advance(metadata: nil) 143 return true 144 case .blockComment(let terminated): 145 let metadata = TokenMetadata( 146 semanticTokenType: .comment, 147 delimitedFoldingRangeKind: .comment 148 ) 149 if terminated { 150 advance(metadata: metadata) 151 } else { 152 advance(error: "Block comment was not terminated") 153 } 154 return true 155 case .lineComment: 156 advance(metadata: TokenMetadata(semanticTokenType: .comment)) 157 return true 158 default: 159 return false 160 } 161 } 162 163 mutating func eatTrivia() { 164 while !isEndOfFile && eatTrivium() {} 165 } 166}