// p4rse, Copyright 2026, Will Hawkins // // This file is part of p4rse. // // This file is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . import Foundation import SystemPackage /// Represent a location in a post-preprocessed piece of P4 source code. public struct SourceLocation: Equatable, CustomStringConvertible { public let range: Range public init(_ start: Int, _ extent: Int) { self.range = start..<(start + extent) } public init(_ range: Range) { self.range = range } public func contains(_ other: SourceLocation) -> Bool { return self.range.contains(other.range) } public var description: String { return "{\(self.range.lowerBound), \(self.self.range.upperBound - self.range.lowerBound)}" } } /// Represent search paths for P4 code that can be accessed with relative paths. public struct SourceManager { let paths: [FilePath] /// Create a `SourceManager` /// /// Any relative `FilePath`s in `paths` will be absolutized /// if a `FileManager` is given. /// /// parameters: /// - paths: The include paths searched for files with relative paths. /// - fm: An optional instance of a `FileManager` that will be used to /// convert relative paths in `paths` to absolute paths. public init(_ paths: [FilePath], _ fm: FileManager? = .none) { // If the user gives a file manager, we will convert relative paths // to absolute paths. Otherwise, we do not. guard let fm else { self.paths = paths return } // There is a file manager, so we should try to absolutize any // relative paths self.paths = paths.map { if !$0.isAbsolute { return FilePath(fm.currentDirectoryPath + "/" + $0.string).lexicallyNormalized() } return $0 } } /// Return `FilePath` of `file` in search paths. /// /// Only if `file` is relative will the search paths be searched. /// /// parameters: /// - file: A file to look for in the search paths. public func firstExisting(_ file: FilePath) -> FilePath? { if file.isAbsolute { return file } let fm = FileManager() for path in self.paths { let combined = path.pushing(file) if fm.fileExists(atPath: combined.string) { return combined } } return .none } } /// Represent preprocessed P4 code /// /// The preprocessed code has metadata to recover the paths of any /// code generated by a preprocessor directive. public struct FileSourceLocation: Equatable, CustomStringConvertible { let location: SourceLocation let path: FilePath let nested: [FileSourceLocation] public init( _ location: SourceLocation, _ path: FilePath, _ nested: [FileSourceLocation] = Array() ) { self.location = location self.path = path self.nested = nested } public func getLocation() -> SourceLocation { return self.location } public func getPath() -> FilePath { return self.path } public func getNestedLocations() -> [FileSourceLocation] { return self.nested } public var description: String { return "\(self.path): \(self.location) (Nested: " + self.nested.map({ location in return "\(location)" }).joined(separator: ",") + ")" } } /// Represent preprocessed P4 code. public struct SourceCode { let code: String let manager: SourceManager let locations: FileSourceLocation public init(_ contents: String, _ manager: SourceManager, _ locations: FileSourceLocation) { self.code = contents self.manager = manager self.locations = locations } public func getManager() -> SourceManager { return self.manager } static func do_annotate( _ contents: String, _ manager: SourceManager, _ locations: FileSourceLocation, _ formatter: Formattable, _ style: Style ) -> String { var result = "" // Keep track of the start of any gap between nested locations. var gap_start = contents.startIndex // contents are devoid of any preceding source code, but the locations do not know that. So, // when we use a range from locations we must adjust appropriately. let offset = locations.location.range.lowerBound for nested in locations.getNestedLocations() { let nested_start = contents.index( contents.startIndex, offsetBy: nested.location.range.lowerBound - offset) let nested_end = contents.index( contents.startIndex, offsetBy: nested.location.range.upperBound - offset) // Add in any gap. result += contents[gap_start.."), initialStyle: Style = Style(StyleColor.Red) ) -> String { if annotated { return SourceCode.do_annotate( self.code, self.manager, self.locations, formatter, initialStyle) } return self.code } public func getLocations() -> FileSourceLocation { return self.locations } } func do_preprocess( _ file: URL, _ manager: SourceManager, _ starting: Int = 0 ) -> Result<(FileSourceLocation, String)> { // First (1) match group has the name of the include file. let re = /\#include[\s]*<([^\s>]*)>/ do { var locations: [FileSourceLocation] = Array() var contents = try String.init(contentsOf: file, encoding: String.defaultCStringEncoding) var changed = true while changed { changed = false for match in contents.matches(of: re) { let before = contents[.. Result { // First, decide whether the given file exists at the path the user gave. let fm = FileManager() let file_to_open = if !fm.fileExists(atPath: file.string) { self.manager.firstExisting(file) } else { file } guard let file_to_open else { return .Error(Error(withMessage: "Could not open \(file) for preprocessing")) } let url = URL.init(filePath: file_to_open.string) switch do_preprocess(url, self.manager) { case .Ok((let location, let contents)): return .Ok(SourceCode(contents, self.manager, location)) case .Error(let e): return .Error(e) } } }