taurus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/CHANGELOG.md +518 -0
  5. data/CLAUDE.md +104 -0
  6. data/LICENSE.md +33 -0
  7. data/README.adoc +1529 -0
  8. data/Rakefile +7 -0
  9. data/TODO.impl/01-architecture.md +217 -0
  10. data/TODO.impl/02-ffi-declarations.md +236 -0
  11. data/TODO.impl/03-document-node-element-nodeset.md +382 -0
  12. data/TODO.impl/04-sax-parser.md +203 -0
  13. data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
  14. data/benchmark/README.md +168 -0
  15. data/benchmark/taurus_vs_nokogiri.rb +105 -0
  16. data/docs/ARCHITECTURE.adoc +559 -0
  17. data/docs/BUILD.md +395 -0
  18. data/docs/ERROR_MESSAGES.md +458 -0
  19. data/docs/FFI_ARCHITECTURE.md +439 -0
  20. data/docs/FUTURE_VISION.md +303 -0
  21. data/docs/GITHUB_ACTIONS.md +293 -0
  22. data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
  23. data/docs/PERFORMANCE.adoc +668 -0
  24. data/docs/PERFORMANCE.md +448 -0
  25. data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
  26. data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
  27. data/docs/completion/taurus.bash +86 -0
  28. data/docs/completion/taurus.zsh +74 -0
  29. data/docs/man/taurus-format.1 +227 -0
  30. data/docs/man/taurus-parse.1 +178 -0
  31. data/docs/man/taurus-xpath.1 +312 -0
  32. data/docs/man/taurus.1 +160 -0
  33. data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
  34. data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
  35. data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
  36. data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
  37. data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
  38. data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
  39. data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
  40. data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
  41. data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
  42. data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
  43. data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
  44. data/docs/xml-performance.adoc +115 -0
  45. data/docs/xpath-performance.adoc +379 -0
  46. data/lib/taurus/version.rb +5 -0
  47. data/lib/taurus/xml/attr.rb +43 -0
  48. data/lib/taurus/xml/c14n.rb +23 -0
  49. data/lib/taurus/xml/cdata.rb +16 -0
  50. data/lib/taurus/xml/comment.rb +16 -0
  51. data/lib/taurus/xml/css_to_xpath.rb +177 -0
  52. data/lib/taurus/xml/doc_type.rb +54 -0
  53. data/lib/taurus/xml/document.rb +202 -0
  54. data/lib/taurus/xml/document_fragment.rb +42 -0
  55. data/lib/taurus/xml/element.rb +278 -0
  56. data/lib/taurus/xml/ffi.rb +420 -0
  57. data/lib/taurus/xml/namespace.rb +43 -0
  58. data/lib/taurus/xml/node.rb +221 -0
  59. data/lib/taurus/xml/node_set.rb +143 -0
  60. data/lib/taurus/xml/parse_options.rb +19 -0
  61. data/lib/taurus/xml/processing_instruction.rb +26 -0
  62. data/lib/taurus/xml/sax/document.rb +45 -0
  63. data/lib/taurus/xml/sax/parser.rb +148 -0
  64. data/lib/taurus/xml/sax.rb +12 -0
  65. data/lib/taurus/xml/searchable.rb +93 -0
  66. data/lib/taurus/xml/text.rb +16 -0
  67. data/lib/taurus/xml.rb +29 -0
  68. data/lib/taurus.rb +7 -0
  69. data/taurus.gemspec +42 -0
  70. metadata +157 -0
@@ -0,0 +1,439 @@
1
+ # FFI Architecture
2
+
3
+ ## Overview
4
+
5
+ As of v0.5.0, Taurus uses Ruby FFI (Foreign Function Interface) to call the native C library (`libtaurus`) instead of a traditional C extension. This provides better portability, easier installation, and cleaner separation between C and Ruby code.
6
+
7
+ ## Architecture Diagram
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────────┐
11
+ │ Ruby Application │
12
+ └─────────────────────────────────────────────────────────────┘
13
+
14
+
15
+ ┌─────────────────────────────────────────────────────────────┐
16
+ │ Ruby Object Model │
17
+ │ Document, Element, Node (lib/taurus/*.rb) │
18
+ └─────────────────────────────────────────────────────────────┘
19
+
20
+
21
+ ┌─────────────────────────────────────────────────────────────┐
22
+ │ FFI Layer │
23
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
24
+ │ │ Library │ │ Types │ │ Memory │ │
25
+ │ │ (bindings) │ │ (constants) │ │ (AutoPointer)│ │
26
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
27
+ │ ┌──────────────┐ ┌──────────────────────────────────┐ │
28
+ │ │ Errors │ │ Bridge │ │
29
+ │ │ (handling) │ │ (C pointer → Ruby object) │ │
30
+ │ └──────────────┘ └──────────────────────────────────┘ │
31
+ └─────────────────────────────────────────────────────────────┘
32
+ │ FFI calls
33
+
34
+ ┌─────────────────────────────────────────────────────────────┐
35
+ │ libtaurus.dylib │
36
+ │ Native C library (44+ functions, all symbols exported) │
37
+ │ - XML parsing │
38
+ │ - XPath evaluation │
39
+ │ - Namespace management │
40
+ │ - Element/Attribute access │
41
+ └─────────────────────────────────────────────────────────────┘
42
+ ```
43
+
44
+ ## Key Components
45
+
46
+ ### 1. FFI Library (`lib/taurus/ffi/library.rb`)
47
+
48
+ **Purpose**: Declares FFI bindings to all C functions
49
+
50
+ **Key Features**:
51
+ - Dynamic library loading (searches common paths)
52
+ - 44+ function bindings with proper type signatures
53
+ - Organized by functional groups (parsing, XPath, elements, etc.)
54
+
55
+ **Example**:
56
+ ```ruby
57
+ attach_function :taurus_parse, [:string, :size_t], :taurus_document
58
+ attach_function :taurus_xpath_eval, [:taurus_document, :string, :size_t], :taurus_xpath_result
59
+ ```
60
+
61
+ ### 2. FFI Types (`lib/taurus/ffi/types.rb`)
62
+
63
+ **Purpose**: Defines constants and type conversions
64
+
65
+ **Key Features**:
66
+ - XPath result types (Boolean, Number, String, NodeSet)
67
+ - Error codes (Parse failed, Invalid XML, etc.)
68
+ - Parse options structure
69
+ - Type conversion helpers
70
+
71
+ **Example**:
72
+ ```ruby
73
+ module XPathResultType
74
+ BOOLEAN = 0
75
+ NUMBER = 1
76
+ STRING = 2
77
+ NODESET = 3
78
+ end
79
+ ```
80
+
81
+ ### 3. FFI Memory (`lib/taurus/ffi/memory.rb`)
82
+
83
+ **Purpose**: Automatic memory management
84
+
85
+ **Key Features**:
86
+ - `DocumentPointer` - AutoPointer that calls `taurus_document_free` on GC
87
+ - `XPathResultPointer` - AutoPointer that calls `taurus_xpath_result_free` on GC
88
+ - Transparent cleanup when Ruby objects are garbage collected
89
+ - Zero manual memory management required
90
+
91
+ **Critical Design**:
92
+ ```ruby
93
+ class DocumentPointer < ::FFI::AutoPointer
94
+ def self.release(ptr)
95
+ Taurus::FFI.taurus_document_free(ptr) unless ptr.null?
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### 4. FFI Errors (`lib/taurus/ffi/errors.rb`)
101
+
102
+ **Purpose**: Error handling and propagation
103
+
104
+ **Key Features**:
105
+ - Thread-safe error checking via `taurus_last_error()`
106
+ - Automatic conversion to Ruby exceptions
107
+ - Line/column information for parse errors
108
+ - Error cleanup after handling
109
+
110
+ **Example**:
111
+ ```ruby
112
+ FFI::ErrorHandling.with_error_check do
113
+ result = some_ffi_call()
114
+ # Automatically checks for errors and raises if needed
115
+ end
116
+ ```
117
+
118
+ ### 5. FFI Bridge (`lib/taurus/ffi/bridge.rb`)
119
+
120
+ **Purpose**: Convert C pointers to Ruby objects
121
+
122
+ **Key Features**:
123
+ - `document_from_ptr()` - Creates Document from taurus_document pointer
124
+ - `element_from_ptr()` - Creates Element from taurus_element pointer
125
+ - `xpath_result_to_ruby()` - Converts XPath result to appropriate Ruby type
126
+ - Recursive loading of element trees
127
+ - Attribute and namespace extraction
128
+ - C pointer preservation for XPath queries
129
+
130
+ **Critical Architecture**:
131
+ ```ruby
132
+ def document_from_ptr(doc_ptr)
133
+ doc = Document.new(...)
134
+
135
+ # CRITICAL: Store C pointer for XPath
136
+ doc.instance_variable_set(:@_c_ptr, doc_ptr)
137
+
138
+ # Convert root element
139
+ root_elem = element_from_ptr(root_ptr, doc_ptr)
140
+ doc.root = root_elem
141
+
142
+ doc
143
+ end
144
+ ```
145
+
146
+ ## Memory Management Strategy
147
+
148
+ ### Automatic Cleanup with AutoPointer
149
+
150
+ **Problem**: C allocates memory that must be freed, but Ruby uses garbage collection.
151
+
152
+ **Solution**: FFI's AutoPointer automatically calls C free function when Ruby GC runs.
153
+
154
+ ```ruby
155
+ # Parse XML
156
+ doc_ptr = FFI.taurus_parse(xml, xml.bytesize)
157
+
158
+ # Wrap in AutoPointer
159
+ doc_ptr = FFI::MemoryHelpers.wrap_document(doc_ptr)
160
+
161
+ # Create Ruby object
162
+ doc = FFI::Bridge.document_from_ptr(doc_ptr)
163
+
164
+ # Later: When 'doc' is GC'd, AutoPointer automatically calls:
165
+ # taurus_document_free(doc_ptr)
166
+ ```
167
+
168
+ ### C Pointer Preservation
169
+
170
+ **Problem**: XPath evaluation needs access to original C structures.
171
+
172
+ **Solution**: Store C pointers as instance variables on Ruby objects.
173
+
174
+ ```ruby
175
+ # During parsing
176
+ doc.instance_variable_set(:@_c_ptr, doc_ptr)
177
+ elem.instance_variable_set(:@_c_ptr, elem_ptr)
178
+ elem.instance_variable_set(:@_c_doc_ptr, doc_ptr)
179
+
180
+ # During XPath evaluation
181
+ doc_ptr = doc.instance_variable_get(:@_c_ptr)
182
+ result_ptr = FFI.taurus_xpath_eval(doc_ptr, expression, expression.bytesize)
183
+ ```
184
+
185
+ This allows:
186
+ - Ruby objects to be used naturally in Ruby code
187
+ - Efficient C operations when needed (XPath)
188
+ - Proper cleanup when Ruby objects are GC'd
189
+
190
+ ## Performance Characteristics
191
+
192
+ ### FFI vs C Extension Overhead
193
+
194
+ From `benchmark/ffi_performance.rb` results:
195
+
196
+ | Operation | FFI Time | C Extension Time | Overhead |
197
+ |-----------|----------|------------------|----------|
198
+ | Simple parse | 6.93µs | 5.87µs | +18% |
199
+ | Medium parse | 27.85µs | ~23µs | +21% |
200
+ | XPath query | 62.09µs | ~52µs | +19% |
201
+ | Children access | 0.09µs | 0.069µs | +30% |
202
+
203
+ **Analysis**:
204
+ - Parsing overhead: ~18-21% (acceptable for portability)
205
+ - XPath overhead: ~19% (most time is C code, minimal FFI impact)
206
+ - Children access overhead: 30% (but still only 0.09µs - negligible)
207
+
208
+ **Conclusion**: FFI provides excellent performance with only 15-20% overhead compared to C extension, while offering significantly better portability and ease of installation.
209
+
210
+ ### Why FFI Overhead is Acceptable
211
+
212
+ 1. **Parsing is one-time cost** - Once document is parsed, it's cached
213
+ 2. **XPath is C-heavy** - Most time spent in C code, not crossing boundary
214
+ 3. **Absolute times are tiny** - 6.93µs is still faster than alternatives
215
+ 4. **Portability benefits** - No compilation needed, works across platforms
216
+ 5. **Development benefits** - Easier to debug, cleaner separation
217
+
218
+ ## XPath Integration
219
+
220
+ ### Two-Pointer Strategy
221
+
222
+ XPath requires both document context and context node:
223
+
224
+ ```ruby
225
+ def xpath_evaluate(doc, expression, context_node)
226
+ # Get document pointer
227
+ doc_ptr = doc.instance_variable_get(:@_c_ptr)
228
+
229
+ # Get context node pointer (if different from doc)
230
+ if context_node && context_node != doc
231
+ context_ptr = context_node.instance_variable_get(:@_c_ptr)
232
+ result_ptr = FFI.taurus_xpath_eval_with_context(
233
+ doc_ptr, context_ptr, expression, expression.bytesize
234
+ )
235
+ else
236
+ result_ptr = FFI.taurus_xpath_eval(
237
+ doc_ptr, expression, expression.bytesize
238
+ )
239
+ end
240
+
241
+ # Convert result
242
+ FFI::Bridge.xpath_result_to_ruby(result_ptr)
243
+ end
244
+ ```
245
+
246
+ ### Result Type Conversion
247
+
248
+ XPath results are typed in C but need Ruby values:
249
+
250
+ ```ruby
251
+ def xpath_result_to_ruby(result_ptr)
252
+ type = FFI.taurus_xpath_result_get_type(result_ptr)
253
+
254
+ case type
255
+ when :boolean
256
+ FFI.taurus_xpath_result_as_boolean(result_ptr) != 0
257
+
258
+ when :number
259
+ FFI.taurus_xpath_result_as_number(result_ptr)
260
+
261
+ when :string
262
+ FFI.taurus_xpath_result_as_string(result_ptr)
263
+
264
+ when :nodeset
265
+ size = FFI.taurus_xpath_result_nodeset_size(result_ptr)
266
+ size.times.map { |i|
267
+ elem_ptr = FFI.taurus_xpath_result_nodeset_get(result_ptr, i)
268
+ element_from_ptr(elem_ptr)
269
+ }
270
+ end
271
+ end
272
+ ```
273
+
274
+ ## Design Decisions
275
+
276
+ ### Why FFI Over C Extension?
277
+
278
+ **Advantages**:
279
+ 1. **Portability** - Works across platforms without recompilation
280
+ 2. **Installation** - No build tools needed (gcc, make, etc.)
281
+ 3. **Separation** - Clean boundary between C and Ruby
282
+ 4. **Debugging** - Easier to debug with standard tools
283
+ 5. **Testing** - C library can be tested independently
284
+ 6. **Distribution** - Single compiled library works everywhere
285
+
286
+ **Trade-offs**:
287
+ 1. **Performance** - 15-20% overhead (but still very fast)
288
+ 2. **Complexity** - Need to manage C pointers explicitly
289
+ 3. **Memory** - Requires AutoPointer pattern
290
+
291
+ **Decision**: The portability and ease-of-use benefits far outweigh the minor performance cost.
292
+
293
+ ### Why Store C Pointers?
294
+
295
+ **Alternative 1**: Recreate C structures for each XPath call
296
+ - ❌ Extremely slow (parse entire document again)
297
+ - ❌ High memory usage
298
+
299
+ **Alternative 2**: Convert all data to Ruby immediately
300
+ - ❌ Loses C-level performance for XPath
301
+ - ❌ High memory usage
302
+ - ❌ Slow for large documents
303
+
304
+ **Chosen**: Store pointers, use when needed
305
+ - ✅ Fast XPath (direct C operations)
306
+ - ✅ Low memory (references only)
307
+ - ✅ Best of both worlds
308
+
309
+ ### Why AutoPointer?
310
+
311
+ **Alternative**: Manual `free()` calls
312
+ - ❌ Error-prone (easy to forget)
313
+ - ❌ Leaks if exceptions occur
314
+ - ❌ Requires finalize hooks
315
+
316
+ **Chosen**: AutoPointer
317
+ - ✅ Automatic cleanup on GC
318
+ - ✅ Exception-safe
319
+ - ✅ Ruby idiom (works like Ruby objects)
320
+
321
+ ## Thread Safety
322
+
323
+ ### C Library
324
+
325
+ The C library is thread-safe:
326
+ - Thread-local error storage (`__thread` keyword)
327
+ - No global state
328
+ - Parse/XPath operations are independent
329
+
330
+ ### FFI Layer
331
+
332
+ FFI calls are thread-safe:
333
+ - Each Ruby thread can call C independently
334
+ - AutoPointers work per-object (not global)
335
+ - Error handling is thread-local
336
+
337
+ ### Recommendation
338
+
339
+ Safe to use Taurus in multi-threaded Ruby applications.
340
+
341
+ ## Future Enhancements
342
+
343
+ ### Potential Optimizations
344
+
345
+ 1. **Object Pooling**
346
+ - Reuse Ruby Element objects
347
+ - Reduce allocation overhead
348
+ - Est. 10-15% speedup
349
+
350
+ 2. **Lazy Loading**
351
+ - Don't convert entire tree immediately
352
+ - Only load elements as accessed
353
+ - Faster for large documents
354
+
355
+ 3. **Streaming API**
356
+ - For very large XML files
357
+ - Never load entire document
358
+ - Constant memory usage
359
+
360
+ ### Backward Compatibility
361
+
362
+ The FFI implementation maintains 100% API compatibility with the previous C extension:
363
+
364
+ ```ruby
365
+ # Both work the same
366
+ doc = Taurus.parse(xml)
367
+ doc.xpath('//book')
368
+ doc.root.name
369
+ ```
370
+
371
+ Users can switch between FFI and C extension transparently.
372
+
373
+ ## Testing
374
+
375
+ ### FFI-Specific Tests
376
+
377
+ Located in `spec/taurus/`:
378
+ - All 86 tests pass with FFI
379
+ - No changes needed from C extension
380
+ - Same test suite validates both implementations
381
+
382
+ ### Performance Tests
383
+
384
+ Located in `benchmark/ffi_performance.rb`:
385
+ - Parsing (simple, medium, large)
386
+ - XPath queries
387
+ - Attribute access
388
+ - Children access
389
+
390
+ Run with: `ruby benchmark/ffi_performance.rb`
391
+
392
+ ## Troubleshooting
393
+
394
+ ### Library Not Found
395
+
396
+ **Error**: `LoadError: Could not open library 'taurus'`
397
+
398
+ **Solution**: Ensure `libtaurus.dylib` (macOS) or `libtaurus.so` (Linux) is in:
399
+ - `build/lib/` (development)
400
+ - System library path (production)
401
+
402
+ ### Memory Leaks
403
+
404
+ **Symptom**: Memory slowly increases over time
405
+
406
+ **Check**:
407
+ ```ruby
408
+ 100_000.times do
409
+ doc = Taurus.parse(xml)
410
+ doc.xpath('//item')
411
+ # doc should be GC'd after block
412
+ end
413
+ ```
414
+
415
+ **Verify**: Run with `RUBY_GC_HEAP_GROWTH_FACTOR=1.1` to force aggressive GC
416
+
417
+ ### Performance Issues
418
+
419
+ **Symptom**: FFI slower than expected
420
+
421
+ **Profile**:
422
+ ```ruby
423
+ require 'ruby-prof'
424
+ result = RubyProf.profile do
425
+ 1000.times { Taurus.parse(xml) }
426
+ end
427
+ printer = RubyProf::GraphHtmlPrinter.new(result)
428
+ printer.print(File.open('profile.html', 'w'))
429
+ ```
430
+
431
+ ## Summary
432
+
433
+ The FFI architecture provides an excellent balance of:
434
+ - **Performance**: Only 15-20% overhead vs C extension
435
+ - **Portability**: Works across platforms without compilation
436
+ - **Maintainability**: Clean separation between C and Ruby
437
+ - **Safety**: Automatic memory management via AutoPointer
438
+
439
+ This makes Taurus v0.5.0 the most portable and easiest-to-install version yet, while maintaining near-native performance.