Skip to content

Commit e9e8e08

Browse files
authored
A tale of two pretty printers and one executable (#97)
Canonical literal and pattern DSL pretty-printers `PrintAsCanonical` will pretty-print an AST using Swift's preferred, canonical regex literal syntax. The parser accepts many things, but Swift can have an opinion on which is the preferred way to spell something. `PrintAsPattern` will print as a result builder DSL. It is parameterized over a maximum top-down levels to convert (everything below that will be a canonical regex literal) as well as a minimum-tree-height value (everything below _that_ will also be printed as a canonical regex literal). Also included is an executable to drive these API. Naming scheme: AST's `renderFoo` will take options and output a String. AST value's `_canonicalBase` produces a string of that value in Swift's canonical regex form. PrettyPrinter's `output` will accumulate the given contents without indentation, newlines, and internal state updates. PrettyPrinter's `print` will indent, insert newlines, and update internal state.
1 parent 47978c7 commit e9e8e08

16 files changed

+1017
-288
lines changed

Package.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,6 @@ let package = Package(
4747
.target(
4848
name: "Prototypes",
4949
dependencies: ["_MatchingEngine"]),
50-
.testTarget(
51-
name: "AlgorithmsTests",
52-
dependencies: ["_StringProcessing"]),
5350
.testTarget(
5451
name: "UnicodeTests",
5552
dependencies: ["_Unicode"]),
@@ -60,6 +57,12 @@ let package = Package(
6057
dependencies: [
6158
.product(name: "ArgumentParser", package: "swift-argument-parser")
6259
]),
60+
.executableTarget(
61+
name: "PatternConverter",
62+
dependencies: [
63+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
64+
"_MatchingEngine",
65+
]),
6366

6467
// MARK: Exercises
6568
.target(
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// swift run PatternConverter <regex>
2+
3+
import ArgumentParser
4+
import _MatchingEngine
5+
6+
@main
7+
struct PatternConverter: ParsableCommand {
8+
9+
@Argument(help: "The regex to convert")
10+
var regex: String
11+
12+
@Flag(help: "Whether to use the experimental syntax")
13+
var experimentalSyntax: Bool = false
14+
15+
@Flag(help: "Whether to show rendered source ranges")
16+
var renderSourceRanges: Bool = false
17+
18+
@Flag(help: "Whether to show canonical regex literal")
19+
var showCanonical: Bool = false
20+
21+
@Option(help: "Limit (from top-down) the conversion levels")
22+
var topDownConversionLimit: Int?
23+
24+
@Option(help: "(TODO) Limit (from bottom-up) the conversion levels")
25+
var bottomUpConversionLimit: Int?
26+
27+
func run() throws {
28+
print("""
29+
30+
NOTE: This tool is experimental and its output is not
31+
necessarily compilable.
32+
33+
""")
34+
let delim = experimentalSyntax ? "|" : "/"
35+
print("Converting '\(delim)\(regex)\(delim)'")
36+
37+
let ast = try _MatchingEngine.parse(
38+
regex,
39+
experimentalSyntax ? .experimental : .traditional)
40+
41+
// Show rendered source ranges
42+
if renderSourceRanges {
43+
print()
44+
print(regex)
45+
print(ast._render(in: regex).joined(separator: "\n"))
46+
print()
47+
}
48+
49+
if showCanonical {
50+
print("Canonical:")
51+
print()
52+
print(ast.renderAsCanonical())
53+
print()
54+
}
55+
56+
print()
57+
let render = ast.renderAsPattern(
58+
maxTopDownLevels: topDownConversionLimit,
59+
minBottomUpLevels: bottomUpConversionLimit
60+
)
61+
print(render)
62+
63+
return
64+
}
65+
}

Sources/_MatchingEngine/Regex/AST/AST.swift

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ extension AST {
5555
}
5656
}
5757

58+
func `as`<T: _ASTNode>(_ t: T.Type = T.self) -> T? {
59+
_associatedValue as? T
60+
}
61+
5862
/// If this node is a parent node, access its children
5963
public var children: [AST]? {
6064
return (_associatedValue as? _ASTParent)?.children
@@ -94,8 +98,6 @@ extension AST {
9498
self.children = mems
9599
self.location = location
96100
}
97-
98-
public var _dumpBase: String { "alternation" }
99101
}
100102

101103
public struct Concatenation: Hashable, _ASTNode {
@@ -106,8 +108,6 @@ extension AST {
106108
self.children = mems
107109
self.location = location
108110
}
109-
110-
public var _dumpBase: String { "" }
111111
}
112112

113113
public struct Quote: Hashable, _ASTNode {
@@ -118,8 +118,6 @@ extension AST {
118118
self.literal = s
119119
self.location = location
120120
}
121-
122-
public var _dumpBase: String { "quote" }
123121
}
124122

125123
public struct Trivia: Hashable, _ASTNode {
@@ -135,11 +133,6 @@ extension AST {
135133
self.contents = v.value
136134
self.location = v.location
137135
}
138-
139-
public var _dumpBase: String {
140-
// TODO: comments, non-semantic whitespace, etc.
141-
""
142-
}
143136
}
144137

145138
public struct Empty: Hashable, _ASTNode {
@@ -148,8 +141,6 @@ extension AST {
148141
public init(_ location: SourceLocation) {
149142
self.location = location
150143
}
151-
152-
public var _dumpBase: String { "" }
153144
}
154145
}
155146

Sources/_MatchingEngine/Regex/AST/ASTProtocols.swift

Lines changed: 0 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -29,112 +29,3 @@ extension AST.Group: _ASTParent {
2929
extension AST.Quantification: _ASTParent {
3030
var children: [AST] { [child] }
3131
}
32-
33-
34-
// MARK: - Printing
35-
36-
/// AST entities can be pretty-printed or dumped
37-
///
38-
/// Alternative: just use `description` for pretty-print
39-
/// and `debugDescription` for dump
40-
public protocol _ASTPrintable:
41-
CustomStringConvertible,
42-
CustomDebugStringConvertible
43-
{
44-
// The "base" dump out for AST nodes, like `alternation`.
45-
// Children printing, parens, etc., handled automatically
46-
var _dumpBase: String { get }
47-
48-
}
49-
extension _ASTPrintable {
50-
public var description: String { _print() }
51-
public var debugDescription: String { _dump() }
52-
53-
var _children: [AST]? {
54-
if let children = (self as? _ASTParent)?.children {
55-
return children
56-
}
57-
if let children = (self as? AST)?.children {
58-
return children
59-
}
60-
return nil
61-
}
62-
63-
func _print() -> String {
64-
// TODO: prettier printing
65-
_dump()
66-
}
67-
func _dump() -> String {
68-
guard let children = _children else {
69-
return _dumpBase
70-
}
71-
let sub = children.lazy.compactMap {
72-
// Exclude trivia for now, as we don't want it to appear when performing
73-
// comparisons of dumped output in tests.
74-
// TODO: We should eventually have some way of filtering out trivia for
75-
// tests, so that it can appear in regular dumps.
76-
if $0.isTrivia { return nil }
77-
return $0._dump()
78-
}.joined(separator: ",")
79-
return "\(_dumpBase)(\(sub))"
80-
}
81-
}
82-
83-
extension AST: _ASTPrintable {
84-
public var _dumpBase: String {
85-
_associatedValue._dumpBase
86-
}
87-
}
88-
89-
// MARK: - Rendering
90-
91-
// Useful for testing, debugging, etc.
92-
extension AST {
93-
func _postOrder() -> Array<AST> {
94-
var nodes = Array<AST>()
95-
_postOrder(into: &nodes)
96-
return nodes
97-
}
98-
func _postOrder(into array: inout Array<AST>) {
99-
children?.forEach { $0._postOrder(into: &array) }
100-
array.append(self)
101-
}
102-
103-
// We render from top-to-bottom, coalescing siblings
104-
public func _render(in input: String) -> [String] {
105-
let base = String(repeating: " ", count: input.count)
106-
var lines = [base]
107-
108-
// TODO: drop the filtering when fake-ness is taken out of
109-
// this module
110-
let nodes = _postOrder().filter(\.location.isReal)
111-
112-
nodes.forEach { node in
113-
let loc = node.location
114-
let count = input[loc.range].count
115-
for idx in lines.indices {
116-
if lines[idx][loc.range].all(\.isWhitespace) {
117-
node._renderRange(count: count, into: &lines[idx])
118-
return
119-
}
120-
}
121-
var nextLine = base
122-
node._renderRange(count: count, into: &nextLine)
123-
lines.append(nextLine)
124-
}
125-
126-
return lines.first!.all(\.isWhitespace) ? [] : lines
127-
}
128-
129-
// Produce a textually "rendered" rane
130-
//
131-
// NOTE: `input` must be the string from which a
132-
// source range was derived.
133-
func _renderRange(
134-
count: Int, into output: inout String
135-
) {
136-
guard count > 0 else { return }
137-
let repl = String(repeating: "-", count: count-1) + "^"
138-
output.replaceSubrange(location.range, with: repl)
139-
}
140-
}

0 commit comments

Comments
 (0)