Ditch InlineArray3

Turns out Array is still faster
This commit is contained in:
Shadowfacts 2023-11-26 22:35:10 -05:00
parent 1c461041c1
commit 29a065049e
5 changed files with 8 additions and 267 deletions

View File

@ -24,8 +24,8 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
private var tokenizer: Tokenizer<String.Iterator>!
private var str: NSMutableAttributedString!
private var actionStack: InlineArray3<ElementAction> = []
private var styleStack: InlineArray3<Style> = []
private var actionStack: [ElementAction] = []
private var styleStack: [Style] = []
// The current run of text w/o styles changing
private var currentRun: String = ""
@ -77,7 +77,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
return str
}
private mutating func handleStartTag(_ name: String, attributes: InlineArray3<HTMLStreamer.Attribute>) {
private mutating func handleStartTag(_ name: String, attributes: [HTMLStreamer.Attribute]) {
switch name {
case "br":
currentRun.append("\n")
@ -287,7 +287,7 @@ public struct AttributedStringConverter<Callbacks: AttributedStringCallbacks> {
public protocol AttributedStringCallbacks {
static func makeURL(string: String) -> URL?
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction
}
public enum ElementAction: Equatable {
@ -308,7 +308,7 @@ public extension AttributedStringCallbacks {
static func makeURL(string: String) -> URL? {
URL(string: string)
}
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction {
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
.default
}
}

View File

@ -1,220 +0,0 @@
//
// InlineArray3.swift
// HTMLStreamer
//
// Created by Shadowfacts on 11/19/23.
//
import Foundation
/// An array with inline space for up to 3 elements.
///
/// If the array grows beyond 3 elements, it will be stored out-of-line.
/// Once that happens, the array will never return to being stored inline,
/// since the allocation cost has already been paid.
public struct InlineArray3<E> {
private var storage: Storage
public init() {
self.storage = .inline(nil, nil, nil)
}
}
extension InlineArray3 {
fileprivate enum Storage {
case inline(Element?, Element?, Element?)
case array(ContiguousArray<Element>)
}
}
extension InlineArray3: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Element...) {
switch elements.count {
case 0:
self.storage = .inline(nil, nil, nil)
case 1:
self.storage = .inline(elements[0], nil, nil)
case 2:
self.storage = .inline(elements[0], elements[1], nil)
case 3:
self.storage = .inline(elements[0], elements[1], elements[2])
default:
self.storage = .array(.init(elements))
}
}
}
extension InlineArray3: MutableCollection {
public typealias Element = E
public typealias Index = Int
public typealias Indices = Range<Int>
public subscript(position: Int) -> Element {
_read {
precondition(position < endIndex)
switch storage {
case .inline(let a, let b, let c):
switch position {
case 0:
yield a.unsafelyUnwrapped
case 1:
yield b.unsafelyUnwrapped
case 2:
yield c.unsafelyUnwrapped
default:
fatalError("unreachable")
}
case .array(let arr):
yield arr[position]
}
}
_modify {
precondition(position < endIndex)
switch storage {
case .inline(let a, let b, let c):
switch position {
case 0:
var newValue = a.unsafelyUnwrapped
yield &newValue
storage = .inline(newValue, b, c)
case 1:
var newValue = b.unsafelyUnwrapped
yield &newValue
storage = .inline(a, newValue, c)
case 2:
var newValue = c.unsafelyUnwrapped
yield &newValue
storage = .inline(a, b, newValue)
default:
fatalError("unreachable")
}
case .array(var arr):
yield &arr[position]
}
}
}
public var startIndex: Int {
0
}
public var endIndex: Int {
switch storage {
case .inline(let a, let b, let c):
a == nil ? 0 : b == nil ? 1 : c == nil ? 2 : 3
case .array(let arr):
arr.endIndex
}
}
}
extension InlineArray3: BidirectionalCollection {
}
extension InlineArray3: RandomAccessCollection {
}
extension InlineArray3: RangeReplaceableCollection {
public mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C: Collection, Element == C.Element {
switch storage {
case .array(var arr):
arr.replaceSubrange(subrange, with: newElements)
storage = .array(arr)
case .inline(var a, var b, var c):
if count - subrange.count + newElements.count <= 3 {
// remove elements at subrange indices
if subrange.contains(2) {
c = nil
}
if subrange.contains(1) {
b = c
c = nil
}
if subrange.contains(0) {
a = b
b = c
c = nil
}
// insert newElements starting at subrange.lowerBound
for (offset, el) in newElements.enumerated() {
// assert that we have space to insert
assert(c == nil)
let newIndex = subrange.lowerBound + offset
switch newIndex {
case 2:
c = el
case 1:
c = b
b = el
case 0:
c = b
b = a
a = el
default:
fatalError("unreachable")
}
}
storage = .inline(a, b, c)
} else {
var arr: ContiguousArray = if let a {
if let b {
if let c {
[a, b, c]
} else {
[a, b]
}
} else {
[a]
}
} else {
[]
}
arr.replaceSubrange(subrange, with: newElements)
storage = .array(arr)
}
}
}
}
private extension Collection {
func safeIndex(_ index: Index, offsetBy: Int) -> Index? {
var index = index
var offsetBy = offsetBy
while offsetBy > 0 {
if index < endIndex {
formIndex(after: &index)
offsetBy -= 1
} else {
return nil
}
}
return index >= endIndex ? nil : index
}
}
extension InlineArray3.Storage: Equatable where E: Equatable {
}
extension InlineArray3: Equatable where E: Equatable {
}
extension InlineArray3: CustomStringConvertible {
public var description: String {
switch storage {
case .inline(nil, nil, nil):
return "[]"
case .inline(.some(let a), nil, nil):
return "[\(a)]"
case .inline(.some(let a), .some(let b), nil):
return "[\(a), \(b)]"
case .inline(.some(let a), .some(let b), .some(let c)):
return "[\(a), \(b), \(c)]"
case .inline(_, _, _):
fatalError("InlineArray3 invariant violated")
case .array(let arr):
return arr.description
}
}
}

View File

@ -11,7 +11,7 @@ struct Tokenizer<Chars: IteratorProtocol<Character>>: IteratorProtocol {
typealias Element = Token
private var chars: Chars
private var reconsumeStack: InlineArray3<Character> = []
private var reconsumeStack: [Character] = []
private var state = State.data
private var returnState: State?
private var temporaryBuffer: String?
@ -195,7 +195,7 @@ struct Tokenizer<Chars: IteratorProtocol<Character>>: IteratorProtocol {
enum Token: Equatable {
case character(Character)
case comment(String)
case startTag(String, selfClosing: Bool, attributes: InlineArray3<Attribute>)
case startTag(String, selfClosing: Bool, attributes: [Attribute])
case endTag(String)
case doctype(String, forceQuirks: Bool, publicIdentifier: String?, systemIdentifier: String?)
}
@ -561,7 +561,6 @@ private extension Tokenizer {
switch c {
case .some("0"..."9"), .some("a"..."z"), .some("A"..."Z"):
if case .attributeValue(_) = returnState {
// TODO: append the current input character to the current attribute's value
if case .startTag(let s, let selfClosing, var attributes) = currentToken {
attributes[attributes.count - 1].value.append(c!)
currentToken = .startTag(s, selfClosing: selfClosing, attributes: attributes)

View File

@ -175,7 +175,7 @@ final class AttributedStringConverterTests: XCTestCase {
func testElementActionCallback() {
struct Callbacks: AttributedStringCallbacks {
static func elementAction(name: String, attributes: InlineArray3<Attribute>) -> ElementAction {
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
let clazz = attributes.attributeValue(for: "class")
if clazz == "invisible" {
return .skip

View File

@ -1,38 +0,0 @@
//
// InlineArray3Tests.swift
//
//
// Created by Shadowfacts on 11/19/23.
//
import XCTest
@testable import HTMLStreamer
final class InlineArray3Tests: XCTestCase {
func testReplaceSubrange() {
// same size
var a: InlineArray3 = [0, 1, 2]
a.replaceSubrange(0..<2, with: [3, 4])
XCTAssertEqual(a, [3, 4, 2])
// grow
a = [0, 1]
a.replaceSubrange(1..<2, with: [2, 3])
XCTAssertEqual(a, [0, 2, 3])
// shrink
a = [0, 1, 2]
a.replaceSubrange(0..<2, with: [])
XCTAssertEqual(a, [2])
a.removeFirst()
XCTAssertEqual(a, [])
}
func testRemoveLast() {
var a: InlineArray3 = [0, 1, 2]
a.removeLast(2)
XCTAssertEqual(a, [0])
}
}