libdictenstein 4.0.0.rc.4
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.
- checksums.yaml +7 -0
- data/README.md +190 -0
- data/bin/libdictenstein-collection-profile +157 -0
- data/lib/vinary_tree/libdictenstein/native/darwin-arm64/liblibdictenstein.dylib +0 -0
- data/lib/vinary_tree/libdictenstein/native/linux-arm64/liblibdictenstein.so +0 -0
- data/lib/vinary_tree/libdictenstein/native/linux-x64/liblibdictenstein.so +0 -0
- data/lib/vinary_tree/libdictenstein/native/windows-x64/libdictenstein.dll +0 -0
- data/lib/vinary_tree/libdictenstein/native.rb +102 -0
- data/lib/vinary_tree/libdictenstein/version.rb +5 -0
- data/lib/vinary_tree/libdictenstein.rb +475 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 010cb3470993511a75d84982f225e366d1d9d31232b858539758a675d51132ba
|
|
4
|
+
data.tar.gz: 53a12f44a10d6fb1059e8322e5608f48d2a28f8bf76b2f9bc8889e4433b56656
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 98bccac45121c8b16fbcefb357b0f9f95c67c2873c35f8e67cb66fa96ad1e5c99108598a3435c8bea008dfce8133765a7005c5a6513bd7474c648e8fbc148850
|
|
7
|
+
data.tar.gz: ff10f78c20b60cf9a150aa697e1072c34a6e9c20d5a92fc2af493fc517b93d856e2e8615ea8b3b59a39c1a766a3fe15912c7eb02d23a63d322d4af10861d05bd
|
data/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Vinary Tree libdictenstein for Ruby
|
|
2
|
+
|
|
3
|
+
The gem exposes full DynamicDAWG CRUD, immutable DoubleArrayTrie construction,
|
|
4
|
+
SCDAWG substring search, persistent ARTrie CRUD/checkpoint/reopen, and persistent
|
|
5
|
+
vocabulary reverse lookup. Every object implements `with_resource`, allowing an
|
|
6
|
+
independently packaged liblevenshtein transducer to retain it in O(1).
|
|
7
|
+
|
|
8
|
+
Calls acquire only a short lifetime lease; operations on the same dictionary
|
|
9
|
+
are not serialized. The project-owned native resource advertises parallel and
|
|
10
|
+
reentrant access automatically.
|
|
11
|
+
|
|
12
|
+
Every dictionary is an `Enumerable` over immutable-revision `Entry` records:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
dictionary = VinaryTree::Libdictenstein::DynamicDawg.new
|
|
16
|
+
dictionary.put("cat", 0)
|
|
17
|
+
dictionary.put("cut", nil)
|
|
18
|
+
|
|
19
|
+
dictionary.each do |entry|
|
|
20
|
+
p [entry.key, entry.value]
|
|
21
|
+
break if entry.key == "cat" # ensure closes the native cursor
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
keys = dictionary.keys
|
|
25
|
+
values = dictionary.values
|
|
26
|
+
snapshot = dictionary.entries
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`each` returns an `Enumerator` without a block. `entry_stream` exposes manual
|
|
30
|
+
`next`, `cancel`, and `close` for pull-driven bounded traversal.
|
|
31
|
+
|
|
32
|
+
<!-- BEGIN GENERATED BINDING OPERATIONS; DO NOT EDIT -->
|
|
33
|
+
|
|
34
|
+
## Support and package contract
|
|
35
|
+
|
|
36
|
+
| Property | Contract |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Binding | Ruby |
|
|
39
|
+
| Languages/runtime | Ruby 3.3+ |
|
|
40
|
+
| Support tier | Tier 2 |
|
|
41
|
+
| Distribution | RubyGems `libdictenstein` |
|
|
42
|
+
| Native boundary | Fiddle over the stable C ABI |
|
|
43
|
+
| Canonical facade source | [`bindings/ruby/lib/vinary_tree/libdictenstein`](../../bindings/ruby/lib/vinary_tree/libdictenstein) |
|
|
44
|
+
|
|
45
|
+
All tiers implement the same ownership, snapshot, status, and compatibility
|
|
46
|
+
laws. The tier controls release gating rather than semantic quality. Start with
|
|
47
|
+
the [producer documentation hub](../../docs/bindings/README.md), then use the
|
|
48
|
+
[`ldict_*` C ABI reference](../../docs/bindings/c-abi-reference.md) for exact
|
|
49
|
+
preconditions, statuses, thread-safety, and complexity.
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
## Installation and native loading
|
|
54
|
+
|
|
55
|
+
Install the distribution named above and its exact `vinary-tree-interop`
|
|
56
|
+
dependency. Published managed packages carry or resolve supported native
|
|
57
|
+
artifacts; source builds use the release library from `target/release` or the
|
|
58
|
+
installed CMake/pkg-config package. Diagnose loading in this order: toolchain
|
|
59
|
+
version, OS/CPU artifact, dependent package pin, loader path, then ABI/API
|
|
60
|
+
handshake. Never silently load an arbitrary same-named system library.
|
|
61
|
+
|
|
62
|
+
## Executable example and verification
|
|
63
|
+
|
|
64
|
+
The canonical checked example is [`bindings/ruby/test/test_conformance.rb`](../../bindings/ruby/test/test_conformance.rb). CI runs
|
|
65
|
+
the public package path with:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
ruby -Ibindings/ruby/lib bindings/ruby/test/test_conformance.rb
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The example is also conformance evidence: it uses public constructors, checks
|
|
72
|
+
membership/value behavior, exports a retained resource, and closes every owned
|
|
73
|
+
handle. Cross-project suites pass that resource to liblevenshtein without
|
|
74
|
+
serialization.
|
|
75
|
+
|
|
76
|
+
## Public API, backends, and data domains
|
|
77
|
+
|
|
78
|
+
| Concept | Semantics |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Dictionary handle | Owns one mutable or immutable backend instance and exposes kind/capability introspection. |
|
|
81
|
+
| CRUD and batch mutation | Text and `u64` operations preserve optional values; empty-batch and partial-failure behavior follows the C reference. |
|
|
82
|
+
| Persistent maintenance | `checkpoint`, `compact`, and `clear` are capability-gated and report unsupported operations explicitly. |
|
|
83
|
+
| Retained resource | `resource()` lends `vt.dictionary.v1`; a consumer retains and snapshots it independently. |
|
|
84
|
+
| Snapshot | Immutable revision with stable node identifiers, exact domains, bounded edge pages, and optional mapped values. |
|
|
85
|
+
|
|
86
|
+
Dynamic DAWG supports mutable finite-term dictionaries; double-array tries are
|
|
87
|
+
read-optimized static structures; SCDAWG indexes substrings; persistent ARTrie
|
|
88
|
+
and vocabulary stores provide durable byte/Unicode/`u64` domains. Select by
|
|
89
|
+
reported kind and capabilities rather than assuming every operation exists.
|
|
90
|
+
|
|
91
|
+
Text APIs validate UTF-8 and traverse Unicode scalar values. Byte APIs retain
|
|
92
|
+
arbitrary octets. Token APIs preserve the full `u64` range. Optional dictionary
|
|
93
|
+
values are represented separately from terminal membership, so `None` is not a
|
|
94
|
+
sentinel and empty terms remain valid when supported.
|
|
95
|
+
|
|
96
|
+
## Native collection surface
|
|
97
|
+
|
|
98
|
+
Every dictionary includes `Enumerable`. `each` returns an `Enumerator`
|
|
99
|
+
without a block and yields host-owned `Entry` records in lexical order; its
|
|
100
|
+
`ensure` path closes the cursor after exhaustion, `break`, or exception.
|
|
101
|
+
`entry_stream` also exposes explicit `next`, `cancel`, and `close`, while
|
|
102
|
+
`entries`, `keys`, and `values` provide materialized snapshot idioms. Binary
|
|
103
|
+
strings, UTF-8 strings, and `Array<Integer>` preserve the three unit domains;
|
|
104
|
+
`nil` remains distinct from every mapped integer.
|
|
105
|
+
|
|
106
|
+
The gem executable keeps construction and warmup outside the timed drain and
|
|
107
|
+
prints one JSON record. Run its materialized, streaming, and early-cancel arms
|
|
108
|
+
over the shared deterministic corpus with:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
ruby bindings/ruby/bin/libdictenstein-collection-profile --arm materialized --entries 4096
|
|
112
|
+
ruby bindings/ruby/bin/libdictenstein-collection-profile --arm stream --entries 65536 --batch-size 256
|
|
113
|
+
ruby bindings/ruby/bin/libdictenstein-collection-profile --arm stream-cancel --entries 65536 --batch-size 64 --early-cancel 64
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The pure Rust producer is the semantic and performance baseline: generic
|
|
117
|
+
snapshot traversal, borrowed and snapshot-owning `IntoIterator`, optimized bulk
|
|
118
|
+
`FromIterator`/`Extend` where infallible, named fallible variants for persistent
|
|
119
|
+
stores, deterministic order, and reusable fold/visitor paths. Read the local
|
|
120
|
+
[Rust API audit](../../docs/bindings/rust-api-idioms.md) and the family
|
|
121
|
+
[collection-protocol design](https://github.com/vinary-tree/liblevenshtein-rust/blob/master/docs/bindings/collection-protocols.md).
|
|
122
|
+
|
|
123
|
+
Public spelling must remain native to this ecosystem. The shared engine
|
|
124
|
+
standardizes laws and batching; it does not expose C handles, vtables, leases,
|
|
125
|
+
or status codes to ordinary application code. Documentation may mark a protocol
|
|
126
|
+
as shipped only after its language conformance and performance gates pass.
|
|
127
|
+
|
|
128
|
+
## Ownership, snapshots, and resource handoff
|
|
129
|
+
|
|
130
|
+
Prefer block forms or `ensure { dictionary.close }`; close persistent stores explicitly.
|
|
131
|
+
|
|
132
|
+
An exported resource is borrowed until its base-vtable `retain` succeeds. A
|
|
133
|
+
captured snapshot arrives owning one retain and may outlive the mutable
|
|
134
|
+
dictionary. Later inserts, updates, removals, compaction, or checkpoints do not
|
|
135
|
+
alter a pinned revision. Every successful retain has exactly one release, and
|
|
136
|
+
failed construction transfers no ownership.
|
|
137
|
+
|
|
138
|
+
## Errors and failure containment
|
|
139
|
+
|
|
140
|
+
Failures become typed Ruby exceptions with status and diagnostic. Branch on the typed status or exception, not diagnostic text.
|
|
141
|
+
Invalid UTF-8, domain mismatch, unsupported capability, closed handle, bad
|
|
142
|
+
path, allocation failure, provider fault, I/O failure, and contained panic are
|
|
143
|
+
distinct cases. Copy thread-local diagnostics before another native call.
|
|
144
|
+
|
|
145
|
+
## Concurrency and reentrancy
|
|
146
|
+
|
|
147
|
+
Independent handles and immutable snapshots are reentrant. Mutations follow
|
|
148
|
+
the backend's advertised synchronization strategy; one host wrapper must not
|
|
149
|
+
invent a stronger promise. Snapshot capture is a linearization point and never
|
|
150
|
+
permits a torn root/count pair. Do not race close against another operation on
|
|
151
|
+
the same handle, and do not retain callback/paging buffers after return.
|
|
152
|
+
|
|
153
|
+
## Performance, durability, and marshalling
|
|
154
|
+
|
|
155
|
+
- Use bulk construction for an initially empty dictionary and presorted input
|
|
156
|
+
when available; unordered construction uses the optimized sort-plus-minimal
|
|
157
|
+
path.
|
|
158
|
+
- Batch mutations to amortize foreign-boundary crossings.
|
|
159
|
+
- Export retained resources instead of serializing dictionaries between
|
|
160
|
+
Vinary packages.
|
|
161
|
+
- Keep byte, Unicode, and `u64` domains explicit to avoid transcoding.
|
|
162
|
+
- Treat `checkpoint` as durability, `compact` as representation maintenance,
|
|
163
|
+
and `close` as ownership release; they are not interchangeable.
|
|
164
|
+
|
|
165
|
+
## Security model
|
|
166
|
+
|
|
167
|
+
Treat paths, terms, values, page offsets, serialized files, and foreign callers
|
|
168
|
+
as untrusted. Validate lengths before allocation, contain panics at the ABI,
|
|
169
|
+
bound diagnostics and batches, prevent path traversal, and reject unknown enum
|
|
170
|
+
values. See the [FFI boundary analysis](../../docs/security/ffi-boundary.md) and
|
|
171
|
+
[family security model](https://github.com/vinary-tree/vinary-tree-interop/blob/master/docs/security-model.md).
|
|
172
|
+
|
|
173
|
+
## Compatibility and troubleshooting
|
|
174
|
+
|
|
175
|
+
The project ABI, project API revision, family ABI, interface version, package
|
|
176
|
+
version, and persistent format version are separate counters. Negotiate each
|
|
177
|
+
at its documented boundary. For unexpected behavior, record dictionary kind,
|
|
178
|
+
capabilities, unit domain, persistence path, exact status, and copied diagnostic
|
|
179
|
+
before reducing the operation sequence.
|
|
180
|
+
|
|
181
|
+
## Maintainer checklist
|
|
182
|
+
|
|
183
|
+
1. Update `bindings/api.json` before changing a public facade or package pin.
|
|
184
|
+
2. Regenerate headers/constants and run the binding contract gate.
|
|
185
|
+
3. Extend the executable, negative-path, leak, and cross-project tests.
|
|
186
|
+
4. Update this guide when ownership, errors, capabilities, or platforms change.
|
|
187
|
+
5. Render PlantUML headlessly and run math/link/documentation checks.
|
|
188
|
+
6. Verify staged registry artifacts contain the guide and coherent pins.
|
|
189
|
+
|
|
190
|
+
<!-- END GENERATED BINDING OPERATIONS -->
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Measures the public Ruby collection facade over a deterministic dictionary
|
|
4
|
+
# revision. Corpus construction and warmup are outside the reported interval.
|
|
5
|
+
|
|
6
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
7
|
+
|
|
8
|
+
require "json"
|
|
9
|
+
require "optparse"
|
|
10
|
+
require "vinary_tree/libdictenstein"
|
|
11
|
+
|
|
12
|
+
LD = VinaryTree::Libdictenstein
|
|
13
|
+
|
|
14
|
+
DEFAULT_ENTRIES = 65_536
|
|
15
|
+
DEFAULT_BATCH_SIZE = 256
|
|
16
|
+
DEFAULT_EARLY_CANCEL = 64
|
|
17
|
+
KEY_UNITS = 38
|
|
18
|
+
U64_MASK = (1 << 64) - 1
|
|
19
|
+
ARMS = %w[materialized stream stream-cancel].freeze
|
|
20
|
+
|
|
21
|
+
def parse_arguments(arguments)
|
|
22
|
+
config = {
|
|
23
|
+
arm: nil,
|
|
24
|
+
entries: DEFAULT_ENTRIES,
|
|
25
|
+
passes: 1,
|
|
26
|
+
warmup_passes: 1,
|
|
27
|
+
batch_size: DEFAULT_BATCH_SIZE,
|
|
28
|
+
early_cancel: DEFAULT_EARLY_CANCEL
|
|
29
|
+
}
|
|
30
|
+
parser = OptionParser.new do |options|
|
|
31
|
+
options.on("--arm ARM") { |value| config[:arm] = value }
|
|
32
|
+
options.on("--entries N", Integer) { |value| config[:entries] = value }
|
|
33
|
+
options.on("--passes N", Integer) { |value| config[:passes] = value }
|
|
34
|
+
options.on("--warmup-passes N", Integer) { |value| config[:warmup_passes] = value }
|
|
35
|
+
options.on("--batch-size N", Integer) { |value| config[:batch_size] = value }
|
|
36
|
+
options.on("--early-cancel N", Integer) { |value| config[:early_cancel] = value }
|
|
37
|
+
end
|
|
38
|
+
parser.parse!(arguments)
|
|
39
|
+
raise OptionParser::InvalidArgument, "unexpected positional arguments" unless arguments.empty?
|
|
40
|
+
raise OptionParser::InvalidArgument, "--arm must be #{ARMS.join(', ')}" unless ARMS.include?(config[:arm])
|
|
41
|
+
%i[entries passes batch_size early_cancel].each do |name|
|
|
42
|
+
raise OptionParser::InvalidArgument, "--#{name.to_s.tr('_', '-')} must be positive" unless config[name].positive?
|
|
43
|
+
end
|
|
44
|
+
raise OptionParser::InvalidArgument, "--warmup-passes must be nonnegative" if config[:warmup_passes].negative?
|
|
45
|
+
config
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def make_corpus(size)
|
|
49
|
+
Array.new(size) do |index|
|
|
50
|
+
[format("collection/%04x/%08x/shared-suffix", index & 0x0fff, index).b, index]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def expected_checksum(corpus, limit)
|
|
55
|
+
corpus.sort_by(&:first).first(limit).sum { |key, value| key.bytesize ^ value } & U64_MASK
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def entry_checksum(entry)
|
|
59
|
+
raise "benchmark expected a byte-domain entry" unless entry.domain == LD::BYTE
|
|
60
|
+
(entry.key.bytesize ^ (entry.value || 0)) & U64_MASK
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drain_materialized(dictionary)
|
|
64
|
+
entries = dictionary.entries
|
|
65
|
+
[entries.sum { |entry| entry_checksum(entry) } & U64_MASK, entries.length]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def drain_stream(dictionary, batch_size:, limit:, cancel:)
|
|
69
|
+
stream = dictionary.entry_stream(
|
|
70
|
+
max_entries: batch_size,
|
|
71
|
+
max_units: batch_size * KEY_UNITS,
|
|
72
|
+
max_values: batch_size
|
|
73
|
+
)
|
|
74
|
+
checksum = 0
|
|
75
|
+
processed = 0
|
|
76
|
+
begin
|
|
77
|
+
while processed < limit && (entry = stream.next)
|
|
78
|
+
checksum = (checksum + entry_checksum(entry)) & U64_MASK
|
|
79
|
+
processed += 1
|
|
80
|
+
end
|
|
81
|
+
if cancel
|
|
82
|
+
stream.cancel
|
|
83
|
+
elsif processed != limit || !stream.next.nil?
|
|
84
|
+
raise "stream cardinality differs from the generated corpus"
|
|
85
|
+
end
|
|
86
|
+
stream.close
|
|
87
|
+
ensure
|
|
88
|
+
stream.close
|
|
89
|
+
end
|
|
90
|
+
[checksum, processed]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def drain(dictionary, config)
|
|
94
|
+
case config[:arm]
|
|
95
|
+
when "materialized"
|
|
96
|
+
drain_materialized(dictionary)
|
|
97
|
+
when "stream"
|
|
98
|
+
drain_stream(dictionary, batch_size: config[:batch_size], limit: config[:entries], cancel: false)
|
|
99
|
+
when "stream-cancel"
|
|
100
|
+
drain_stream(
|
|
101
|
+
dictionary,
|
|
102
|
+
batch_size: config[:batch_size],
|
|
103
|
+
limit: [config[:entries], config[:early_cancel]].min,
|
|
104
|
+
cancel: true
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def execute(arguments)
|
|
110
|
+
config = parse_arguments(arguments)
|
|
111
|
+
corpus = make_corpus(config[:entries])
|
|
112
|
+
dictionary = LD::DynamicDawg.new(domain: LD::BYTE)
|
|
113
|
+
begin
|
|
114
|
+
inserted = dictionary.put_all(corpus)
|
|
115
|
+
raise "inserted #{inserted} of #{corpus.length} generated entries" unless inserted == corpus.length
|
|
116
|
+
|
|
117
|
+
consumed = config[:arm] == "stream-cancel" ? [config[:entries], config[:early_cancel]].min : config[:entries]
|
|
118
|
+
expected = expected_checksum(corpus, consumed)
|
|
119
|
+
config[:warmup_passes].times do
|
|
120
|
+
checksum, count = drain(dictionary, config)
|
|
121
|
+
raise "warmup checksum or cardinality mismatch" unless count == consumed && checksum == expected
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
125
|
+
checksum = 0
|
|
126
|
+
config[:passes].times do
|
|
127
|
+
pass_checksum, count = drain(dictionary, config)
|
|
128
|
+
raise "timed checksum or cardinality mismatch" unless count == consumed && pass_checksum == expected
|
|
129
|
+
checksum = (checksum + pass_checksum) & U64_MASK
|
|
130
|
+
end
|
|
131
|
+
elapsed_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - started
|
|
132
|
+
raise "aggregate checksum mismatch" unless checksum == (expected * config[:passes]) & U64_MASK
|
|
133
|
+
|
|
134
|
+
puts JSON.generate(
|
|
135
|
+
schema: "libdictenstein.host-collection-traversal.v1",
|
|
136
|
+
runtime: "ruby",
|
|
137
|
+
arm: config[:arm],
|
|
138
|
+
dictionary_entries: config[:entries],
|
|
139
|
+
consumed_entries_per_pass: consumed,
|
|
140
|
+
passes: config[:passes],
|
|
141
|
+
warmup_passes: config[:warmup_passes],
|
|
142
|
+
batch_size: config[:arm] == "materialized" ? nil : config[:batch_size],
|
|
143
|
+
early_cancel: config[:arm] == "stream-cancel" ? config[:early_cancel] : nil,
|
|
144
|
+
elapsed_ns: elapsed_ns,
|
|
145
|
+
checksum: checksum
|
|
146
|
+
)
|
|
147
|
+
ensure
|
|
148
|
+
dictionary.close
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
execute(ARGV)
|
|
154
|
+
rescue StandardError => error
|
|
155
|
+
warn error.message
|
|
156
|
+
exit 2
|
|
157
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "fiddle/import"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
|
|
4
|
+
module VinaryTree
|
|
5
|
+
module Libdictenstein
|
|
6
|
+
module Native
|
|
7
|
+
extend Fiddle::Importer
|
|
8
|
+
|
|
9
|
+
def self.candidates
|
|
10
|
+
explicit = ENV["LIBDICTENSTEIN_LIBRARY"]
|
|
11
|
+
os, filename = case RbConfig::CONFIG["host_os"]
|
|
12
|
+
when /darwin/ then ["darwin", "liblibdictenstein.dylib"]
|
|
13
|
+
when /mswin|mingw/ then ["windows", "libdictenstein.dll"]
|
|
14
|
+
else ["linux", "liblibdictenstein.so"]
|
|
15
|
+
end
|
|
16
|
+
cpu = case RbConfig::CONFIG["host_cpu"]
|
|
17
|
+
when /x86_64|amd64/ then "x64"
|
|
18
|
+
when /aarch64|arm64/ then "arm64"
|
|
19
|
+
else RbConfig::CONFIG["host_cpu"]
|
|
20
|
+
end
|
|
21
|
+
platform = "#{os}-#{cpu}"
|
|
22
|
+
[explicit, File.expand_path("native/#{platform}/#{filename}", __dir__), filename].compact
|
|
23
|
+
end
|
|
24
|
+
loaded = candidates.find do |candidate|
|
|
25
|
+
begin dlload candidate; true
|
|
26
|
+
rescue Fiddle::DLError then false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
raise Fiddle::DLError, "unable to load libdictenstein (tried #{candidates.join(', ')})" unless loaded
|
|
30
|
+
|
|
31
|
+
Resource = struct ["void* context", "void* vtable"]
|
|
32
|
+
TextEntry = struct [
|
|
33
|
+
"void* data", "size_t len", "uint64_t value", "uint8_t has_value",
|
|
34
|
+
"uint8_t reserved[7]"
|
|
35
|
+
]
|
|
36
|
+
DictionaryEntry = struct [
|
|
37
|
+
"size_t unit_offset", "size_t unit_len", "size_t value_offset",
|
|
38
|
+
"size_t value_len", "uint64_t reserved"
|
|
39
|
+
]
|
|
40
|
+
EntryBatchLimits = struct [
|
|
41
|
+
"size_t max_entries", "size_t max_units", "size_t max_values",
|
|
42
|
+
"uint64_t reserved"
|
|
43
|
+
]
|
|
44
|
+
EntryBatch = struct [
|
|
45
|
+
"void* entries", "size_t entry_count", "void* units",
|
|
46
|
+
"size_t unit_count", "void* values", "size_t value_count",
|
|
47
|
+
"uint64_t generation", "uint64_t reserved"
|
|
48
|
+
]
|
|
49
|
+
EntriesInfo = struct [
|
|
50
|
+
"uint32_t unit_domain", "uint32_t value_domain", "uint32_t order",
|
|
51
|
+
"uint32_t reserved0", "uint64_t flags", "size_t exact_len",
|
|
52
|
+
"uint64_t identity_producer", "uint64_t identity_revision",
|
|
53
|
+
"uint64_t reserved[2]"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
extern "uint32_t ldict_abi_version(void)"
|
|
57
|
+
extern "uint32_t ldict_api_revision(void)"
|
|
58
|
+
extern "const char* ldict_last_error_message(void)"
|
|
59
|
+
extern "uint32_t ldict_dynamic_dawg_new(uint32_t, void*)"
|
|
60
|
+
extern "uint32_t ldict_double_array_trie_new(uint32_t, void*, size_t, void*)"
|
|
61
|
+
extern "uint32_t ldict_scdawg_new(uint32_t, void*)"
|
|
62
|
+
extern "uint32_t ldict_persistent_artrie_create(uint32_t, const void*, size_t, void*)"
|
|
63
|
+
extern "uint32_t ldict_persistent_artrie_open(uint32_t, const void*, size_t, void*)"
|
|
64
|
+
extern "uint32_t ldict_persistent_vocab_create(const void*, size_t, void*)"
|
|
65
|
+
extern "uint32_t ldict_persistent_vocab_open(const void*, size_t, void*)"
|
|
66
|
+
extern "void ldict_dictionary_free(void*)"
|
|
67
|
+
extern "uint32_t ldict_dictionary_resource(void*, void*)"
|
|
68
|
+
extern "uint32_t ldict_dictionary_entries_open(void*, void*, void*)"
|
|
69
|
+
extern "uint32_t ldict_entry_cursor_next(void*, void*, void*)"
|
|
70
|
+
extern "uint32_t ldict_entry_cursor_release(void*, uint64_t)"
|
|
71
|
+
extern "uint32_t ldict_entry_cursor_cancel(void*)"
|
|
72
|
+
extern "uint32_t ldict_entry_cursor_free(void*)"
|
|
73
|
+
extern "uint32_t ldict_dictionary_kind(void*, void*)"
|
|
74
|
+
extern "uint32_t ldict_dictionary_capabilities(void*, void*)"
|
|
75
|
+
extern "uint32_t ldict_dictionary_len(void*, void*)"
|
|
76
|
+
extern "uint32_t ldict_dictionary_insert_text_value(void*, const void*, size_t, uint64_t, uint8_t, void*)"
|
|
77
|
+
extern "uint32_t ldict_dictionary_remove_text(void*, const void*, size_t, void*)"
|
|
78
|
+
extern "uint32_t ldict_dictionary_contains_text(void*, const void*, size_t, void*)"
|
|
79
|
+
extern "uint32_t ldict_dictionary_get_text_value(void*, const void*, size_t, void*, void*, void*)"
|
|
80
|
+
extern "uint32_t ldict_dictionary_insert_u64_value(void*, const void*, size_t, uint64_t, uint8_t, void*)"
|
|
81
|
+
extern "uint32_t ldict_dictionary_remove_u64(void*, const void*, size_t, void*)"
|
|
82
|
+
extern "uint32_t ldict_dictionary_contains_u64(void*, const void*, size_t, void*)"
|
|
83
|
+
extern "uint32_t ldict_dictionary_get_u64_value(void*, const void*, size_t, void*, void*, void*)"
|
|
84
|
+
extern "uint32_t ldict_dictionary_insert_text_batch(void*, void*, size_t, void*)"
|
|
85
|
+
extern "uint32_t ldict_dictionary_clear(void*)"
|
|
86
|
+
extern "uint32_t ldict_dictionary_compact(void*, void*)"
|
|
87
|
+
extern "uint32_t ldict_dictionary_checkpoint(void*)"
|
|
88
|
+
extern "uint32_t ldict_scdawg_contains_substring(void*, const void*, size_t, void*)"
|
|
89
|
+
extern "uint32_t ldict_scdawg_substring_frequency(void*, const void*, size_t, void*)"
|
|
90
|
+
extern "uint32_t ldict_vocab_get_term(void*, uint64_t, void*, size_t, void*, void*)"
|
|
91
|
+
|
|
92
|
+
module_function
|
|
93
|
+
def pointer_output = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
|
|
94
|
+
def read_pointer(output) = output[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
|
|
95
|
+
def size_output = Fiddle::Pointer.malloc(Fiddle::SIZEOF_SIZE_T, Fiddle::RUBY_FREE)
|
|
96
|
+
def read_size(output) = output[0, Fiddle::SIZEOF_SIZE_T].unpack1("J")
|
|
97
|
+
def u64_output = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
98
|
+
def read_u64(output) = output[0, 8].unpack1("Q")
|
|
99
|
+
def byte_output = Fiddle::Pointer.malloc(1, Fiddle::RUBY_FREE)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
require_relative "libdictenstein/version"
|
|
3
|
+
require_relative "libdictenstein/native"
|
|
4
|
+
|
|
5
|
+
module VinaryTree
|
|
6
|
+
module Libdictenstein
|
|
7
|
+
BYTE = 1
|
|
8
|
+
UNICODE_SCALAR = 2
|
|
9
|
+
U64 = 3
|
|
10
|
+
|
|
11
|
+
# Native ABI version (LDICT_ABI_VERSION); always 1 for this family.
|
|
12
|
+
def self.abi_version = Native.ldict_abi_version
|
|
13
|
+
|
|
14
|
+
# Compatible-additions revision within the ABI version (LDICT_API_REVISION).
|
|
15
|
+
def self.api_revision = Native.ldict_api_revision
|
|
16
|
+
|
|
17
|
+
Lookup = Data.define(:found?, :value)
|
|
18
|
+
Entry = Data.define(:key, :value, :domain)
|
|
19
|
+
EntryInfo = Data.define(:domain, :exact_length, :snapshot_identity)
|
|
20
|
+
|
|
21
|
+
class Error < StandardError
|
|
22
|
+
attr_reader :status
|
|
23
|
+
def initialize(status)
|
|
24
|
+
@status = status
|
|
25
|
+
super("libdictenstein status #{status}: #{Native.ldict_last_error_message.to_s}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
def self.check(status) = (raise Error, status unless status.zero?)
|
|
29
|
+
|
|
30
|
+
class ConcurrentHandle
|
|
31
|
+
def initialize(pointer)
|
|
32
|
+
@pointer = pointer
|
|
33
|
+
@active = 0
|
|
34
|
+
@closing = false
|
|
35
|
+
@mutex = Mutex.new
|
|
36
|
+
@condition = ConditionVariable.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with_pointer
|
|
40
|
+
pointer = @mutex.synchronize do
|
|
41
|
+
raise IOError, "dictionary is closed" if @closing || @pointer.zero?
|
|
42
|
+
@active += 1
|
|
43
|
+
@pointer
|
|
44
|
+
end
|
|
45
|
+
yield pointer
|
|
46
|
+
ensure
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
@active -= 1
|
|
49
|
+
@condition.broadcast if @active.zero?
|
|
50
|
+
end if pointer
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def close
|
|
54
|
+
pointer = @mutex.synchronize do
|
|
55
|
+
return 0 if @pointer.zero?
|
|
56
|
+
@closing = true
|
|
57
|
+
@condition.wait(@mutex) until @active.zero?
|
|
58
|
+
result = @pointer
|
|
59
|
+
@pointer = 0
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
Native.ldict_dictionary_free(pointer) unless pointer.zero?
|
|
63
|
+
pointer
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Shared low-level adapter over one opaque native entry cursor. Every key
|
|
68
|
+
# is copied before a leased batch is released.
|
|
69
|
+
class EntryCursorState
|
|
70
|
+
attr_reader :info
|
|
71
|
+
|
|
72
|
+
def initialize(handle, max_entries:, max_units:, max_values:)
|
|
73
|
+
raise ArgumentError, "max_entries must be positive" unless max_entries.positive?
|
|
74
|
+
raise ArgumentError, "entry batch limits must be nonnegative" if max_units.negative? || max_values.negative?
|
|
75
|
+
|
|
76
|
+
@mutex = Mutex.new
|
|
77
|
+
@cursor = 0
|
|
78
|
+
@leased = false
|
|
79
|
+
@ended = false
|
|
80
|
+
@index = 0
|
|
81
|
+
@limits_memory = Fiddle::Pointer.malloc(Native::EntryBatchLimits.size, Fiddle::RUBY_FREE)
|
|
82
|
+
@limits = Native::EntryBatchLimits.new(@limits_memory)
|
|
83
|
+
@limits.max_entries = max_entries
|
|
84
|
+
@limits.max_units = max_units
|
|
85
|
+
@limits.max_values = max_values
|
|
86
|
+
@limits.reserved = 0
|
|
87
|
+
@batch_memory = Fiddle::Pointer.malloc(Native::EntryBatch.size, Fiddle::RUBY_FREE)
|
|
88
|
+
@batch = Native::EntryBatch.new(@batch_memory)
|
|
89
|
+
info_memory = Fiddle::Pointer.malloc(Native::EntriesInfo.size, Fiddle::RUBY_FREE)
|
|
90
|
+
native_info = Native::EntriesInfo.new(info_memory)
|
|
91
|
+
cursor_output = Native.pointer_output
|
|
92
|
+
handle.with_pointer do |dictionary|
|
|
93
|
+
Libdictenstein.check(
|
|
94
|
+
Native.ldict_dictionary_entries_open(dictionary, cursor_output, info_memory)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
@cursor = Native.read_pointer(cursor_output)
|
|
98
|
+
flags = native_info.flags
|
|
99
|
+
@info = EntryInfo.new(
|
|
100
|
+
native_info.unit_domain,
|
|
101
|
+
flags.anybits?(1) ? native_info.exact_len : nil,
|
|
102
|
+
flags.anybits?(2) ? [native_info.identity_producer, native_info.identity_revision].freeze : nil
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def next_entry
|
|
107
|
+
@mutex.synchronize do
|
|
108
|
+
return nil if @cursor.zero? || @ended
|
|
109
|
+
begin
|
|
110
|
+
unless @leased
|
|
111
|
+
status = Native.ldict_entry_cursor_next(@cursor, @limits_memory, @batch_memory)
|
|
112
|
+
if status == 1
|
|
113
|
+
@ended = true
|
|
114
|
+
close_locked(cancel: false)
|
|
115
|
+
return nil
|
|
116
|
+
end
|
|
117
|
+
Libdictenstein.check(status)
|
|
118
|
+
@leased = true
|
|
119
|
+
@index = 0
|
|
120
|
+
end
|
|
121
|
+
result = copy_entry(@index)
|
|
122
|
+
@index += 1
|
|
123
|
+
release_locked if @index == @batch.entry_count
|
|
124
|
+
result
|
|
125
|
+
rescue Exception
|
|
126
|
+
close_locked(cancel: true) rescue nil
|
|
127
|
+
raise
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def cancel
|
|
133
|
+
@mutex.synchronize do
|
|
134
|
+
return nil if @cursor.zero? || @ended
|
|
135
|
+
first_error = native_error(Native.ldict_entry_cursor_cancel(@cursor))
|
|
136
|
+
begin
|
|
137
|
+
release_locked
|
|
138
|
+
rescue Exception => error
|
|
139
|
+
first_error ||= error
|
|
140
|
+
end
|
|
141
|
+
@ended = true
|
|
142
|
+
raise first_error if first_error
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def close
|
|
148
|
+
@mutex.synchronize { close_locked(cancel: true) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def closed?
|
|
152
|
+
@mutex.synchronize { @cursor.zero? }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def address(pointer)
|
|
158
|
+
return 0 if pointer.nil?
|
|
159
|
+
pointer.respond_to?(:to_i) ? pointer.to_i : Integer(pointer)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def checked_range(offset, length, total, name)
|
|
163
|
+
unless offset >= 0 && length >= 0 && offset <= total && length <= total - offset
|
|
164
|
+
raise RuntimeError, "invalid native #{name} arena range"
|
|
165
|
+
end
|
|
166
|
+
offset...(offset + length)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def copy_entry(index)
|
|
170
|
+
raise RuntimeError, "invalid native entry descriptor index" unless index.between?(0, @batch.entry_count - 1)
|
|
171
|
+
# `entries` collides with Fiddle::CStruct#entries; indexed field access
|
|
172
|
+
# selects the actual pointer member.
|
|
173
|
+
entries_address = address(@batch["entries"])
|
|
174
|
+
raise RuntimeError, "native entry descriptor array is null" if entries_address.zero?
|
|
175
|
+
descriptor = Native::DictionaryEntry.new(
|
|
176
|
+
Fiddle::Pointer.new(entries_address + index * Native::DictionaryEntry.size)
|
|
177
|
+
)
|
|
178
|
+
range = checked_range(descriptor.unit_offset, descriptor.unit_len, @batch.unit_count, "unit")
|
|
179
|
+
unit_address = address(@batch.units)
|
|
180
|
+
raise RuntimeError, "native unit arena is null" if range.size.positive? && unit_address.zero?
|
|
181
|
+
key = case @info.domain
|
|
182
|
+
when BYTE
|
|
183
|
+
range.size.zero? ? "".b : Fiddle::Pointer.new(unit_address + range.begin)[0, range.size].b
|
|
184
|
+
when UNICODE_SCALAR
|
|
185
|
+
scalars = range.size.zero? ? [] : Fiddle::Pointer.new(unit_address + range.begin * 4)[0, range.size * 4].unpack("L*")
|
|
186
|
+
scalars.pack("U*")
|
|
187
|
+
when U64
|
|
188
|
+
range.size.zero? ? [] : Fiddle::Pointer.new(unit_address + range.begin * 8)[0, range.size * 8].unpack("Q*")
|
|
189
|
+
else
|
|
190
|
+
raise RuntimeError, "unknown native entry unit domain #{@info.domain}"
|
|
191
|
+
end
|
|
192
|
+
value = case descriptor.value_len
|
|
193
|
+
when 0
|
|
194
|
+
nil
|
|
195
|
+
when 1
|
|
196
|
+
value_range = checked_range(descriptor.value_offset, 1, @batch.value_count, "value")
|
|
197
|
+
values_address = address(@batch.values)
|
|
198
|
+
raise RuntimeError, "native value arena is null" if values_address.zero?
|
|
199
|
+
Fiddle::Pointer.new(values_address + value_range.begin * 8)[0, 8].unpack1("Q")
|
|
200
|
+
else
|
|
201
|
+
raise RuntimeError, "invalid native optional-u64 descriptor"
|
|
202
|
+
end
|
|
203
|
+
Entry.new(key, value, @info.domain)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def release_locked
|
|
207
|
+
return nil unless @leased
|
|
208
|
+
Libdictenstein.check(Native.ldict_entry_cursor_release(@cursor, @batch.generation))
|
|
209
|
+
@leased = false
|
|
210
|
+
@index = 0
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def native_error(status)
|
|
215
|
+
status.zero? ? nil : Error.new(status)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def close_locked(cancel:)
|
|
219
|
+
return nil if @cursor.zero?
|
|
220
|
+
first_error = cancel ? native_error(Native.ldict_entry_cursor_cancel(@cursor)) : nil
|
|
221
|
+
begin
|
|
222
|
+
release_locked
|
|
223
|
+
rescue Exception => error
|
|
224
|
+
first_error ||= error
|
|
225
|
+
end
|
|
226
|
+
free_error = native_error(Native.ldict_entry_cursor_free(@cursor))
|
|
227
|
+
if free_error.nil?
|
|
228
|
+
@cursor = 0
|
|
229
|
+
@ended = true
|
|
230
|
+
else
|
|
231
|
+
first_error ||= free_error
|
|
232
|
+
end
|
|
233
|
+
raise first_error if first_error
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Public bounded stream. Enumerable#each uses ensure so break and raised
|
|
239
|
+
# exceptions deterministically close the native cursor.
|
|
240
|
+
class EntryStream
|
|
241
|
+
include Enumerable
|
|
242
|
+
|
|
243
|
+
attr_reader :info
|
|
244
|
+
|
|
245
|
+
def initialize(handle, max_entries:, max_units:, max_values:)
|
|
246
|
+
@state = EntryCursorState.new(
|
|
247
|
+
handle,
|
|
248
|
+
max_entries: max_entries,
|
|
249
|
+
max_units: max_units,
|
|
250
|
+
max_values: max_values
|
|
251
|
+
)
|
|
252
|
+
@info = @state.info
|
|
253
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@state))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def self.finalizer(state) = proc { state.close rescue nil }
|
|
257
|
+
|
|
258
|
+
def next = @state.next_entry
|
|
259
|
+
|
|
260
|
+
def each
|
|
261
|
+
return enum_for(__method__) unless block_given?
|
|
262
|
+
begin
|
|
263
|
+
while (entry = self.next)
|
|
264
|
+
yield entry
|
|
265
|
+
end
|
|
266
|
+
ensure
|
|
267
|
+
close
|
|
268
|
+
end
|
|
269
|
+
self
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def cancel = @state.cancel
|
|
273
|
+
|
|
274
|
+
def close
|
|
275
|
+
@state.close
|
|
276
|
+
ObjectSpace.undefine_finalizer(self) if @state.closed?
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
class Dictionary
|
|
282
|
+
include Enumerable
|
|
283
|
+
|
|
284
|
+
attr_reader :handle
|
|
285
|
+
def initialize(pointer)
|
|
286
|
+
@handle = ConcurrentHandle.new(pointer)
|
|
287
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@handle))
|
|
288
|
+
end
|
|
289
|
+
def self.finalizer(handle) = proc { handle.close rescue nil }
|
|
290
|
+
|
|
291
|
+
def close
|
|
292
|
+
@handle.close
|
|
293
|
+
ObjectSpace.undefine_finalizer(self)
|
|
294
|
+
nil
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def with_resource
|
|
298
|
+
@handle.with_pointer do |pointer|
|
|
299
|
+
resource = Native::Resource.malloc
|
|
300
|
+
Libdictenstein.check(Native.ldict_dictionary_resource(pointer, resource))
|
|
301
|
+
yield resource.context.to_i, resource.vtable.to_i
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def kind = scalar(:ldict_dictionary_kind, Native.u64_output, ->(output) { Native.read_u64(output) & 0xffff_ffff })
|
|
306
|
+
def capabilities = scalar(:ldict_dictionary_capabilities, Native.u64_output, Native.method(:read_u64))
|
|
307
|
+
def length = scalar(:ldict_dictionary_len, Native.size_output, Native.method(:read_size))
|
|
308
|
+
alias size length
|
|
309
|
+
|
|
310
|
+
def entry_stream(max_entries: 256, max_units: 4096, max_values: 256)
|
|
311
|
+
EntryStream.new(
|
|
312
|
+
@handle,
|
|
313
|
+
max_entries: max_entries,
|
|
314
|
+
max_units: max_units,
|
|
315
|
+
max_values: max_values
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def each
|
|
320
|
+
stream = entry_stream
|
|
321
|
+
return stream.each unless block_given?
|
|
322
|
+
stream.each { |entry| yield entry }
|
|
323
|
+
self
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Host-owned materialized snapshot idioms.
|
|
327
|
+
def entries = each.to_a
|
|
328
|
+
def keys = entries.map(&:key)
|
|
329
|
+
def values = entries.map(&:value)
|
|
330
|
+
|
|
331
|
+
def include?(term)
|
|
332
|
+
output = Native.byte_output
|
|
333
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_contains_text(pointer, term.b, term.bytesize, output)) }
|
|
334
|
+
output[0].positive?
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def get(term)
|
|
338
|
+
found, value, present = Native.byte_output, Native.u64_output, Native.byte_output
|
|
339
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_get_text_value(pointer, term.b, term.bytesize, found, value, present)) }
|
|
340
|
+
Lookup.new(found[0].positive?, present[0].positive? ? Native.read_u64(value) : nil)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def include_u64?(tokens)
|
|
344
|
+
packed = tokens.pack("Q*"); output = Native.byte_output
|
|
345
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_contains_u64(pointer, packed, tokens.length, output)) }
|
|
346
|
+
output[0].positive?
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def get_u64(tokens)
|
|
350
|
+
packed = tokens.pack("Q*"); found, value, present = Native.byte_output, Native.u64_output, Native.byte_output
|
|
351
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_get_u64_value(pointer, packed, tokens.length, found, value, present)) }
|
|
352
|
+
Lookup.new(found[0].positive?, present[0].positive? ? Native.read_u64(value) : nil)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
private
|
|
356
|
+
|
|
357
|
+
def scalar(function, output, decode)
|
|
358
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.public_send(function, pointer, output)) }
|
|
359
|
+
decode.call(output)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def put_text(term, value)
|
|
363
|
+
output = Native.byte_output
|
|
364
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_insert_text_value(pointer, term.b, term.bytesize, value || 0, value.nil? ? 0 : 1, output)) }
|
|
365
|
+
output[0].positive?
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def remove_text(term)
|
|
369
|
+
output = Native.byte_output
|
|
370
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_remove_text(pointer, term.b, term.bytesize, output)) }
|
|
371
|
+
output[0].positive?
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def put_tokens(tokens, value)
|
|
375
|
+
packed = tokens.pack("Q*"); output = Native.byte_output
|
|
376
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_insert_u64_value(pointer, packed, tokens.length, value || 0, value.nil? ? 0 : 1, output)) }
|
|
377
|
+
output[0].positive?
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def remove_tokens(tokens)
|
|
381
|
+
packed = tokens.pack("Q*"); output = Native.byte_output
|
|
382
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_remove_u64(pointer, packed, tokens.length, output)) }
|
|
383
|
+
output[0].positive?
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
class DynamicDawg < Dictionary
|
|
388
|
+
def initialize(domain: UNICODE_SCALAR)
|
|
389
|
+
output = Native.pointer_output; Libdictenstein.check(Native.ldict_dynamic_dawg_new(domain, output)); super(Native.read_pointer(output))
|
|
390
|
+
end
|
|
391
|
+
def put(term, value = nil) = put_text(term, value)
|
|
392
|
+
def remove(term) = remove_text(term)
|
|
393
|
+
def put_u64(tokens, value = nil) = put_tokens(tokens, value)
|
|
394
|
+
def remove_u64(tokens) = remove_tokens(tokens)
|
|
395
|
+
def clear = @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_clear(pointer)) }
|
|
396
|
+
def compact
|
|
397
|
+
output = Native.size_output; @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_compact(pointer, output)) }; Native.read_size(output)
|
|
398
|
+
end
|
|
399
|
+
def put_all(entries)
|
|
400
|
+
terms = entries.map { |term, _value| term.b }
|
|
401
|
+
memory = Fiddle::Pointer.malloc([1, entries.length * Native::TextEntry.size].max, Fiddle::RUBY_FREE)
|
|
402
|
+
entries.each_with_index do |(_term, value), index|
|
|
403
|
+
item = Native::TextEntry.new(memory + index * Native::TextEntry.size)
|
|
404
|
+
item.data = Fiddle::Pointer[terms[index]]
|
|
405
|
+
item.len = terms[index].bytesize
|
|
406
|
+
item.value = value || 0
|
|
407
|
+
item.has_value = value.nil? ? 0 : 1
|
|
408
|
+
end
|
|
409
|
+
output = Native.size_output
|
|
410
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_insert_text_batch(pointer, memory, entries.length, output)) }
|
|
411
|
+
Native.read_size(output)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
class DoubleArrayTrie < Dictionary
|
|
416
|
+
def initialize(entries, domain: UNICODE_SCALAR)
|
|
417
|
+
terms = entries.map { |term, _value| term.b }
|
|
418
|
+
memory = Fiddle::Pointer.malloc([1, entries.length * Native::TextEntry.size].max, Fiddle::RUBY_FREE)
|
|
419
|
+
entries.each_with_index do |(_term, value), index|
|
|
420
|
+
item = Native::TextEntry.new(memory + index * Native::TextEntry.size)
|
|
421
|
+
item.data = Fiddle::Pointer[terms[index]]; item.len = terms[index].bytesize; item.value = value || 0; item.has_value = value.nil? ? 0 : 1
|
|
422
|
+
end
|
|
423
|
+
output = Native.pointer_output; Libdictenstein.check(Native.ldict_double_array_trie_new(domain, memory, entries.length, output)); super(Native.read_pointer(output))
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
class Scdawg < Dictionary
|
|
428
|
+
def initialize(domain: UNICODE_SCALAR)
|
|
429
|
+
output = Native.pointer_output; Libdictenstein.check(Native.ldict_scdawg_new(domain, output)); super(Native.read_pointer(output))
|
|
430
|
+
end
|
|
431
|
+
def put(term, value = nil) = put_text(term, value)
|
|
432
|
+
def include_substring?(term)
|
|
433
|
+
output = Native.byte_output; @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_scdawg_contains_substring(pointer, term.b, term.bytesize, output)) }; output[0].positive?
|
|
434
|
+
end
|
|
435
|
+
def substring_frequency(term)
|
|
436
|
+
output = Native.size_output; @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_scdawg_substring_frequency(pointer, term.b, term.bytesize, output)) }; Native.read_size(output)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
class PersistentArtrie < Dictionary
|
|
441
|
+
def self.create(path, domain: UNICODE_SCALAR) = open_native(path, domain, true)
|
|
442
|
+
def self.open(path, domain: UNICODE_SCALAR) = open_native(path, domain, false)
|
|
443
|
+
def self.open_native(path, domain, create)
|
|
444
|
+
text = File.expand_path(path).b; output = Native.pointer_output
|
|
445
|
+
function = create ? :ldict_persistent_artrie_create : :ldict_persistent_artrie_open
|
|
446
|
+
Libdictenstein.check(Native.public_send(function, domain, text, text.bytesize, output)); new(Native.read_pointer(output))
|
|
447
|
+
end
|
|
448
|
+
def put(term, value = nil) = put_text(term, value)
|
|
449
|
+
def remove(term) = remove_text(term)
|
|
450
|
+
def put_u64(tokens, value = nil) = put_tokens(tokens, value)
|
|
451
|
+
def remove_u64(tokens) = remove_tokens(tokens)
|
|
452
|
+
def checkpoint = @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_checkpoint(pointer)) }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
class PersistentVocabulary < Dictionary
|
|
456
|
+
def self.create(path) = open_native(path, true)
|
|
457
|
+
def self.open(path) = open_native(path, false)
|
|
458
|
+
def self.open_native(path, create)
|
|
459
|
+
text = File.expand_path(path).b; output = Native.pointer_output
|
|
460
|
+
function = create ? :ldict_persistent_vocab_create : :ldict_persistent_vocab_open
|
|
461
|
+
Libdictenstein.check(Native.public_send(function, text, text.bytesize, output)); new(Native.read_pointer(output))
|
|
462
|
+
end
|
|
463
|
+
def put(term, index) = put_text(term, index)
|
|
464
|
+
def term(index)
|
|
465
|
+
length, found = Native.size_output, Native.byte_output
|
|
466
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_vocab_get_term(pointer, index, 0, 0, length, found)) }
|
|
467
|
+
return nil if found[0].zero?
|
|
468
|
+
output = Fiddle::Pointer.malloc(Native.read_size(length), Fiddle::RUBY_FREE)
|
|
469
|
+
@handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_vocab_get_term(pointer, index, output, output.size, length, found)) }
|
|
470
|
+
output[0, Native.read_size(length)].force_encoding(Encoding::UTF_8)
|
|
471
|
+
end
|
|
472
|
+
def checkpoint = @handle.with_pointer { |pointer| Libdictenstein.check(Native.ldict_dictionary_checkpoint(pointer)) }
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: libdictenstein
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 4.0.0.rc.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dylon Edwards
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: DynamicDAWG, DAT, SCDAWG, and persistent ARTrie bindings for Ruby
|
|
13
|
+
email:
|
|
14
|
+
- dylon.devo@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- libdictenstein-collection-profile
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- bin/libdictenstein-collection-profile
|
|
22
|
+
- lib/vinary_tree/libdictenstein.rb
|
|
23
|
+
- lib/vinary_tree/libdictenstein/native.rb
|
|
24
|
+
- lib/vinary_tree/libdictenstein/native/darwin-arm64/liblibdictenstein.dylib
|
|
25
|
+
- lib/vinary_tree/libdictenstein/native/linux-arm64/liblibdictenstein.so
|
|
26
|
+
- lib/vinary_tree/libdictenstein/native/linux-x64/liblibdictenstein.so
|
|
27
|
+
- lib/vinary_tree/libdictenstein/native/windows-x64/libdictenstein.dll
|
|
28
|
+
- lib/vinary_tree/libdictenstein/version.rb
|
|
29
|
+
homepage: https://github.com/vinary-tree/libdictenstein
|
|
30
|
+
licenses:
|
|
31
|
+
- Apache-2.0
|
|
32
|
+
metadata:
|
|
33
|
+
source_code_uri: https://github.com/vinary-tree/libdictenstein
|
|
34
|
+
rubygems_mfa_required: 'true'
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.3'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 3.7.0.dev
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: High-performance modular dictionary backends
|
|
52
|
+
test_files: []
|