compiler: Add Preprocessor Support
TODO: Test that file names are properly tracked in included files. Signed-off-by: Will Hawkins <hawkinsw@obs.cr>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Foundation
|
||||
import System
|
||||
|
||||
/// Represent a location in a post-preprocessed piece of P4 source code.
|
||||
public struct SourceLocation: Equatable, CustomStringConvertible {
|
||||
|
||||
public let range: Range<Int>
|
||||
|
||||
public init(_ start: Int, _ extent: Int) {
|
||||
self.range = start..<(start + extent)
|
||||
}
|
||||
|
||||
public init(_ range: Range<Int>) {
|
||||
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 a set of directories containing P4 code that can be accessed with relative paths.
|
||||
public struct SourceManager {
|
||||
let paths: [FilePath]
|
||||
public init(_ paths: [FilePath]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public func firstExisting(_ file: FilePath) -> FilePath? {
|
||||
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 and retain information about source filenames.
|
||||
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 getSource() -> String {
|
||||
return self.code
|
||||
}
|
||||
|
||||
public func getManager() -> SourceManager {
|
||||
return self.manager
|
||||
}
|
||||
|
||||
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[..<match.range.lowerBound]
|
||||
let after = contents[match.range.upperBound...]
|
||||
|
||||
// Determine whether there is a file with that name in the include path.
|
||||
guard let included_path = manager.firstExisting(FilePath.init("\(match.1)")) else {
|
||||
return .Error(
|
||||
Error(
|
||||
withMessage:
|
||||
"Could not open \(match.1) for inclusion"))
|
||||
}
|
||||
|
||||
// Try to make a url from the configured file.
|
||||
guard let included_url = URL.init(filePath: included_path) else {
|
||||
return .Error(Error(withMessage: "Could not convert \(included_path) into a URL"))
|
||||
}
|
||||
|
||||
// By calling ourselves recursively, the include being processed will
|
||||
// be _completely_ expanded (including any nested includes).
|
||||
switch do_preprocess(included_url, manager, starting + before.count) {
|
||||
case .Ok((let location, let expanded)):
|
||||
// Recombine what was before and after the include being processed
|
||||
// with the expanded text.
|
||||
contents = before + expanded + after
|
||||
// Remember the location (and those it has nested) that were found in
|
||||
// the expanded text.
|
||||
locations.append(location)
|
||||
case .Error(let e):
|
||||
return .Error(e)
|
||||
}
|
||||
|
||||
// Only process one at a time.
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return .Ok(
|
||||
(
|
||||
FileSourceLocation(
|
||||
SourceLocation(starting..<(starting + contents.count)), FilePath(file.path()), locations),
|
||||
contents
|
||||
))
|
||||
|
||||
} catch (let e) {
|
||||
return .Error(Error(withMessage: "\(e)"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Preprocess P4 code.
|
||||
public struct SourceCodePreprocessor {
|
||||
let manager: SourceManager
|
||||
|
||||
public init(_ manager: SourceManager) {
|
||||
self.manager = manager
|
||||
}
|
||||
|
||||
public func preprocess(_ file: FilePath) -> Result<SourceCode> {
|
||||
// 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"))
|
||||
}
|
||||
|
||||
guard let url = URL.init(filePath: file_to_open) else {
|
||||
return .Error(Error(withMessage: "Could not convert \(file_to_open) into a URL"))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user