From 6926feead1a7fadb7f5f974996bef8c30b3bfdbe Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Wed, 9 Jul 2025 01:55:50 +0200 Subject: Use AST based text editor --- Map/Business/WmapParser/Parser.swift | 488 +++++++++++++++++++++++++++++++++++ 1 file changed, 488 insertions(+) create mode 100644 Map/Business/WmapParser/Parser.swift (limited to 'Map/Business/WmapParser/Parser.swift') diff --git a/Map/Business/WmapParser/Parser.swift b/Map/Business/WmapParser/Parser.swift new file mode 100644 index 0000000..f04998c --- /dev/null +++ b/Map/Business/WmapParser/Parser.swift @@ -0,0 +1,488 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program 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://map.tranquil.systems. +import Foundation + +extension Wmap { + class Parser { + private let lexer: Lexer + private var currentToken: Token + private var vertexLabelMap: [String: String] = [:] + + init(lexer: Lexer) { + self.lexer = lexer + self.currentToken = lexer.nextToken() + } + + func parse() -> [Entity] { + var entities: [Entity] = [] + + while currentToken.type != .eof { + if currentToken.type == .newline { + advance() + continue + } + + do { + let entity = try parseEntity() + entities.append(entity) + + if currentToken.type == .newline { + advance() + } else if currentToken.type != .eof { + throw ParseError.expectedNewline(actual: currentToken) + } + } catch { + skipToNextLine() + } + } + + return entities + } + + private func parseEntity() throws -> Entity { + switch currentToken.type { + case .vertexLabel: + return try parseVertexOrEdge() + case .noteKeyword: + return try parseNote() + case .stageNumber: + return try parseStage() + case .groupKeyword: + return try parseGroup() + case .inertiaKeyword: + return try parseInertia() + case .evolutionKeyword: + return try parseEvolution() + case .error: + // Error tokens should cause the line to be ignored + throw ParseError.invalidLine + default: + throw ParseError.invalidLine + } + } + + private func parseVertexOrEdge() throws -> Entity { + let startPos = currentToken.position + let firstLabel = currentToken.value + let firstLabelRange = makeSourceRange(from: currentToken) + + vertexLabelMap[firstLabel.lowercased()] = firstLabel + advance() + + if currentToken.type == .edgeUndirected || currentToken.type == .edgeDirected { + let isDirected = currentToken.type == .edgeDirected + let edgeTypeRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .vertexLabel else { + throw ParseError.expectedVertexLabel(actual: currentToken) + } + + let secondLabel = currentToken.value + let secondLabelRange = makeSourceRange(from: currentToken) + let endPos = currentToken.position + + vertexLabelMap[secondLabel.lowercased()] = secondLabel + advance() + + return Edge( + from: firstLabel, + to: secondLabel, + isDirected: isDirected, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: firstLabelRange.stringRange + .lowerBound.. Note { + let startPos = currentToken.position + let keywordRange = makeSourceRange(from: currentToken) + advance() + + let (coordinates, coordRanges) = try parsePositionWithRanges() + + // For notes, we need to scan the remaining text on the line + let textToken = lexer.scanText() + let text = textToken.value + let textRange = makeSourceRange(from: textToken) + let endPos = textToken.position + currentToken = lexer.nextToken() // Update current token after scanning text + + return Note( + coordinates: coordinates, + text: text, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: keywordRange.stringRange.lowerBound.. Stage { + let startPos = currentToken.position + let stageToken = currentToken + let stageValue = stageToken.value + + // For bracketed stage numbers like [ii], we need to parse the components + // The lexer gives us the full token, but we need to break it down + let tokenRange = stageToken.stringRange + + // Find the bracket positions within the token + let leftBracketRange = SourceRange( + start: stageToken.position, + end: SourcePosition( + line: stageToken.position.line, + column: stageToken.position.column + 1, + stringIndex: lexer.input.index(after: tokenRange.lowerBound) + ), + stringRange: tokenRange.lowerBound.. Group { + let startPos = currentToken.position + let keywordRange = makeSourceRange(from: currentToken) + advance() + + var vertices: [String] = [] + var vertexRanges: [SourceRange] = [] + var commaRanges: [SourceRange] = [] + + guard currentToken.type == .vertexLabel else { + throw ParseError.expectedVertexLabel(actual: currentToken) + } + + vertices.append(currentToken.value) + vertexRanges.append(makeSourceRange(from: currentToken)) + advance() + + while currentToken.type == .comma { + commaRanges.append(makeSourceRange(from: currentToken)) + advance() + + guard currentToken.type == .vertexLabel else { + throw ParseError.expectedVertexLabel(actual: currentToken) + } + + vertices.append(currentToken.value) + vertexRanges.append(makeSourceRange(from: currentToken)) + advance() + } + + let endPos = vertexRanges.last?.end ?? startPos + + return Group( + vertices: vertices, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: keywordRange.stringRange + .lowerBound.. Inertia { + let startPos = currentToken.position + let keywordRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .vertexLabel else { + throw ParseError.expectedVertexLabel(actual: currentToken) + } + + let vertex = currentToken.value + let vertexRange = makeSourceRange(from: currentToken) + let endPos = currentToken.position + advance() + + return Inertia( + vertex: vertex, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: keywordRange.stringRange.lowerBound.. Evolution { + let startPos = currentToken.position + let keywordRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .vertexLabel else { + throw ParseError.expectedVertexLabel(actual: currentToken) + } + + let vertex = currentToken.value + let vertexRange = makeSourceRange(from: currentToken) + advance() + + let isPositive: Bool + let signRange: SourceRange + if currentToken.type == .plus { + isPositive = true + signRange = makeSourceRange(from: currentToken) + advance() + } else if currentToken.type == .minus { + isPositive = false + signRange = makeSourceRange(from: currentToken) + advance() + } else { + throw ParseError.expectedSign(actual: currentToken) + } + + guard currentToken.type == .realNumber else { + throw ParseError.expectedNumber(actual: currentToken) + } + + guard let value = Double(currentToken.value) else { + throw ParseError.invalidNumber(currentToken.value) + } + + let valueRange = makeSourceRange(from: currentToken) + let endPos = currentToken.position + advance() + + return Evolution( + vertex: vertex, + isPositive: isPositive, + value: value, + position: startPos, + range: SourceRange( + start: startPos, end: endPos, + stringRange: keywordRange.stringRange.lowerBound.. ( + (Double, Double), + ( + leftParen: SourceRange, xCoord: SourceRange, comma: SourceRange, yCoord: SourceRange, + rightParen: SourceRange + ) + ) { + guard currentToken.type == .leftParen else { + throw ParseError.expectedLeftParen(actual: currentToken) + } + let leftParenRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .realNumber else { + throw ParseError.expectedNumber(actual: currentToken) + } + guard let x = Double(currentToken.value) else { + throw ParseError.invalidNumber(currentToken.value) + } + let xCoordRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .comma else { + throw ParseError.expectedComma(actual: currentToken) + } + let commaRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .realNumber else { + throw ParseError.expectedNumber(actual: currentToken) + } + guard let y = Double(currentToken.value) else { + throw ParseError.invalidNumber(currentToken.value) + } + let yCoordRange = makeSourceRange(from: currentToken) + advance() + + guard currentToken.type == .rightParen else { + throw ParseError.expectedRightParen(actual: currentToken) + } + let rightParenRange = makeSourceRange(from: currentToken) + advance() + + return ((x, y), (leftParenRange, xCoordRange, commaRange, yCoordRange, rightParenRange)) + } + + private func parseOptionalShapeWithRange() throws -> (String?, SourceRange?) { + guard currentToken.type == .shapeLabel else { + return (nil, nil) + } + + let shape = currentToken.value + let shapeRange = makeSourceRange(from: currentToken) + advance() + return (shape, shapeRange) + } + + private func makeSourceRange(from token: Token) -> SourceRange { + return SourceRange( + start: token.position, + end: SourcePosition( + line: token.position.line, + column: token.position.column + token.value.count, + stringIndex: token.stringRange.upperBound + ), + stringRange: token.stringRange + ) + } + + private func advance() { + currentToken = lexer.nextToken() + } + + private func skipToNextLine() { + while currentToken.type != .newline && currentToken.type != .eof { + advance() + } + if currentToken.type == .newline { + advance() + } + } + } + + enum ParseError: Error, LocalizedError { + case unexpectedToken(expected: String, actual: Token) + case expectedVertexLabel(actual: Token) + case expectedNumber(actual: Token) + case expectedText(actual: Token) + case expectedSign(actual: Token) + case expectedComma(actual: Token) + case expectedLeftParen(actual: Token) + case expectedRightParen(actual: Token) + case expectedNewline(actual: Token) + case invalidNumber(String) + case invalidLine + + var errorDescription: String? { + switch self { + case .unexpectedToken(let expected, let actual): + return + "Expected \(expected), got \(actual.type) '\(actual.value)' at line \(actual.position.line)" + case .expectedVertexLabel(let actual): + return "Expected vertex label, got \(actual.type) at line \(actual.position.line)" + case .invalidNumber(let value): + return "Invalid number format: \(value)" + // ... other cases + default: + return "Parse error" + } + } + } +} -- cgit