1// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers 2// 3// SPDX-License-Identifier: MPL-2.0 4 5import Foundation 6 7public struct Lexer { 8 private let input: String 9 private var index: String.Index 10 private var tokens: [PterodactylSyntax.Token] 11 12 public init(input: String) { 13 self.input = input 14 self.index = input.startIndex 15 self.tokens = [] 16 } 17 18 private var isAtEnd: Bool { index >= input.endIndex } 19 20 private var peek: Character? { 21 guard index < input.endIndex else { return nil } 22 return input[index] 23 } 24 25 private func lookahead() -> Character? { 26 guard index < input.endIndex else { return nil } 27 let next = input.index(after: index) 28 guard next < input.endIndex else { return nil } 29 return input[next] 30 } 31 32 private mutating func advance() -> Character { 33 let c = input[index] 34 index = input.index(after: index) 35 36 return c 37 } 38 39 private mutating func consume(while predicate: (Character) -> Bool) { 40 while let c = peek, predicate(c) { 41 _ = advance() 42 } 43 } 44 45 func text(from start: String.Index) -> String { 46 let range = start..<index 47 return String(input[range]) 48 } 49 50 public mutating func nextToken() -> (kind: TokenKind, text: String)? { 51 guard let c = peek else { 52 return nil 53 } 54 55 let start = index 56 57 if c.isNewline { 58 _ = advance() 59 return (kind: .newline, text: text(from: start)) 60 } 61 62 if c.isWhitespace && !c.isNewline { 63 consume { $0.isWhitespace && !$0.isNewline } 64 return (kind: .whitespace, text: text(from: start)) 65 } 66 67 if c == "/" && lookahead() == "/" { 68 _ = advance() 69 _ = advance() 70 consume { $0 != "\n" } 71 return (kind: .lineComment, text: text(from: start)) 72 } 73 74 if c == "/" && lookahead() == "*" { 75 _ = advance() // consume '/' 76 _ = advance() // consume '*' 77 var terminated = false 78 79 while let ch = peek { 80 if ch == "*" && lookahead() == "/" { 81 _ = advance() // consume '*' 82 _ = advance() // consume '/' 83 terminated = true 84 break 85 } 86 _ = advance() 87 } 88 89 return (kind: .blockComment(terminated: terminated), text: text(from: start)) 90 } 91 92 if c == "<" && lookahead() == "=" { 93 _ = advance() 94 _ = advance() 95 return (kind: .punctuation(.doubleLeftArrow), text: text(from: start)) 96 } 97 98 if c == "=" && lookahead() == ">" { 99 _ = advance() 100 _ = advance() 101 return (kind: .punctuation(.doubleRightArrow), text: text(from: start)) 102 } 103 104 if let punct = Punctuation(rawValue: String(c)) { 105 _ = advance() 106 return (.punctuation(punct), String(c)) 107 } else if c.isLetter || c == "_" { 108 _ = advance() 109 consume { $0.isLetter || $0.isNumber || $0 == "_" } 110 let text = text(from: start) 111 if let keyword = Keyword(rawValue: text) { 112 return (kind: .keyword(keyword), text: text) 113 } 114 return (kind: .identifier, text: text) 115 } 116 117 // Invalid single char (dont drop input) 118 let ch = advance() 119 return (kind: .error, text: String(ch)) 120 } 121 122 public mutating func tokenize() -> [Token] { 123 var tokens: [Token] = [] 124 125 while !isAtEnd { 126 guard let token = nextToken() else { break } 127 tokens.append( 128 Token(kind: token.kind, text: token.text) 129 ) 130 } 131 132 let eofToken = Token(kind: .eof, text: "") 133 tokens.append(eofToken) 134 135 return tokens 136 } 137}