Adding block layout processor

This is as-yet untested.

+100
Sources/PterodactylSyntax/BlockLayoutProcessor.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
fileprivate struct TokenLocation {
+
let startLine: Int
+
let startColumn: Int
+
}
+
+
fileprivate struct LocatedToken {
+
let token: Token
+
let location: TokenLocation
+
let lines: [String.SubSequence]
+
+
init(token: Token, location: TokenLocation) {
+
self.token = token
+
self.location = location
+
self.lines = token.text.split(separator: "\n", omittingEmptySubsequences: false)
+
}
+
+
var nextLocation: TokenLocation {
+
let startLine = location.startLine + lines.count - 1
+
let startColumn = if lines.count > 1 { lines.last!.utf16.count } else { lines.last?.utf16.count ?? location.startColumn }
+
return TokenLocation(startLine: startLine, startColumn: startColumn)
+
}
+
}
+
+
public struct BlockLayoutProcessor {
+
private let locatedTokens: [LocatedToken]
+
+
public init(tokens: [Token]) {
+
var locatedTokens: [LocatedToken] = []
+
var location = TokenLocation(startLine: 0, startColumn: 0)
+
for token in tokens {
+
let locatedToken = LocatedToken(token: token, location: location)
+
locatedTokens.append(locatedToken)
+
location = locatedToken.nextLocation
+
}
+
+
self.locatedTokens = locatedTokens
+
}
+
+
+
public func layout() -> [Token] {
+
var result: [Token] = []
+
var indentStack: [Int] = [0]
+
var previousLine = 0
+
var firstTokenInBlock = false
+
+
for (index, locatedToken) in locatedTokens.enumerated() {
+
guard locatedToken.token.kind != .eof else { break }
+
guard locatedToken.token.kind.canDetermineLayoutColumn else {
+
result.append(locatedToken.token)
+
continue
+
}
+
+
if locatedToken.location.startLine > previousLine {
+
while indentStack.count > 1 && locatedToken.location.startColumn < indentStack.last! {
+
indentStack.removeLast()
+
result.append(Token(kind: .blockEnd, text: ""))
+
}
+
+
if !firstTokenInBlock && indentStack.count > 1 && locatedToken.location.startColumn == indentStack.last! {
+
result.append(Token(kind: .blockSep, text: ""))
+
}
+
}
+
+
result.append(locatedToken.token)
+
+
if locatedToken.token.kind.isBlockHerald {
+
firstTokenInBlock = true
+
+
if let nextToken = locatedTokens[index...].first(where: { $0.location.startLine > locatedToken.location.startLine && $0.token.kind.canDetermineLayoutColumn }) {
+
result.append(Token(kind: .blockBegin, text: ""))
+
indentStack.append(nextToken.location.startColumn)
+
} else {
+
result.append(Token(kind: .blockBegin, text: ""))
+
result.append(Token(kind: .blockEnd, text: ""))
+
}
+
} else {
+
firstTokenInBlock = false
+
}
+
+
previousLine = locatedToken.location.startLine
+
}
+
+
while indentStack.count > 1 {
+
indentStack.removeLast()
+
result.append(Token(kind: .blockEnd, text: ""))
+
}
+
+
if let eof = locatedTokens.last, eof.token.kind == .eof {
+
result.append(eof.token)
+
}
+
+
return result
+
}
+
}
+25
Sources/PterodactylSyntax/Diagnostic.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
public struct Diagnostic: Equatable {
+
enum Severity: Equatable {
+
case error
+
case warning
+
case note
+
}
+
let message: String
+
let severity: Severity
+
/// Absolute UTF-16 code unit offsets from start of source
+
let absoluteRange: Range<Int>
+
+
init(message: String, severity: Severity, absoluteRange: Range<Int>) {
+
self.message = message
+
self.severity = severity
+
self.absoluteRange = absoluteRange
+
}
+
+
init(message: String, absoluteRange: Range<Int>) {
+
self.init(message: message, severity: Severity.error, absoluteRange: absoluteRange)
+
}
+
}
+4 -1
Sources/PterodactylSyntax/Lexer.swift
···
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
-
//
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
//
// SPDX-License-Identifier: MPL-2.0
import Foundation
+12 -10
Sources/PterodactylSyntax/SyntaxTree.swift
···
import Foundation
public struct SyntaxTree: Codable, Sendable {
-
public let kind: SyntaxTreeKind
-
public let metadata: SyntaxTreeMetadata?
-
public let children: [Child]
-
public let utf16Length: Int
-
+
public var kind: SyntaxTreeKind
+
public var metadata: SyntaxTreeMetadata?
+
public var children: [Child]
+
+
public var utf16Length: Int {
+
children.reduce(0) { length, child in
+
length + child.utf16Length
+
}
+
}
+
public enum Child: Codable, Sendable {
-
case token(Token, metadata: TokenMetadata?)
+
case token(Token, metadata: TokenMetadata? = nil)
case tree(SyntaxTree)
}
-
public init(kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata?, children: [SyntaxTree.Child]) {
+
public init(kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata? = nil, children: [SyntaxTree.Child]) {
self.kind = kind
self.metadata = metadata
self.children = children
-
self.utf16Length = children.reduce(0) { length, child in
-
length + child.utf16Length
-
}
}
}
+34 -2
Sources/PterodactylSyntax/Types.swift
···
public enum Keyword: Codable, Equatable, Sendable {
case `import`
+
case theory
+
case `where`
}
public enum Whitespace: Codable, Equatable, Sendable {
···
case error
case identifier
case whitespace(Whitespace)
+
case blockBegin
+
case blockEnd
+
case blockSep
}
-
public enum SyntaxTreeKind: Codable, Equatable, Sendable {
-
case error
+
public final class SyntaxTreeKind: Codable, Equatable, Sendable {
+
static let error: SyntaxTreeKind = .init(name: "error")
+
+
public static func == (lhs: SyntaxTreeKind, rhs: SyntaxTreeKind) -> Bool {
+
lhs === rhs
+
}
+
+
let name: String
+
var description: String { name }
+
+
required init(name: String) {
+
self.name = name
+
}
}
public struct TokenMetadata: Codable, Equatable, Sendable {
···
public struct SyntaxTreeMetadata: Codable, Equatable, Sendable {
}
+
+
extension TokenKind {
+
var canDetermineLayoutColumn: Bool {
+
switch self {
+
case .whitespace, .eof: false
+
default: true
+
}
+
}
+
+
var isBlockHerald: Bool {
+
switch self {
+
case .keyword(.where): true
+
default: false
+
}
+
}
+
}