Tagging and Querying
This page is the API-focused reference for the metadata plane. For the conceptual overview, start with Metadata and Protocol Composition.
What Tags And Queries Are For
Tags are structured classical facts attached to quantum slots. Queries search for those facts by exact value, wildcard, or predicate. This is how independently written protocols discover resources without holding direct references to each other.
In practice, this means a protocol can ask for:
- "a slot entangled with node 7",
- "any slot carrying this protocol-specific marker",
- or "the newest matching resource that is unlocked and assigned".
That is more composable than hard-wiring protocol-to-protocol calls.
Two Different Places Metadata Lives
There are two closely related cases:
- tags on
RegRefslots, added withtag!, which describe quantum resources; - messages in a
MessageBuffer, inserted withput!, which describe incoming classical communication.
The querying interface works across both, but the result shape is different:
- querying a register returns the matching
slot,id,tag, andtime; - querying a message buffer returns the matching
srcandtag, plus an internaldepthused when deleting the message.
The Smallest Useful Workflow
tag!(reg[1], :ready, 7)
tag!(reg[2], :ready, 9)
query(reg, :ready, 7)
queryall(reg, :ready, ❓)
querydelete!(reg, :ready, 9)This is the core composition pattern in QuantumSavory: produce metadata, query for metadata, optionally consume it.
Tag Shapes
The Tag type stores a small structured payload. Common patterns are:
- symbolic tags such as
Tag(:ready)orTag(:swap_request); - typed tags such as
Tag(EntanglementCounterpart, remote_node, remote_slot).
Typed tags are especially useful when several protocols share a common metadata schema. They make the intended meaning explicit and allow custom printing.
Wildcards And Predicates
Queries can match exactly, use a wildcard, or use a predicate for one field.
query(reg, :ready, ❓)
query(reg, EntanglementCounterpart, 7, ❓)
query(reg, :score, x -> x > 90)This is the part that makes the metadata plane flexible. Protocols can agree on the meaning of a tag without agreeing on one exact hard-coded lookup path.
Register Queries Versus Message Queries
For registers:
queryreturns the first match;queryallreturns all matches;querydelete!returns one match and removes it.
For message buffers:
queryis available, butquerydelete!is usually the useful operation, because classical messages are often consumed once handled.
If you want to wait until a match exists, use query_wait or querydelete_wait! from the discrete-event layer.
Consuming Register Tags In Protocols
Register query results are snapshots. They include a tag id and a slot, but another process can run after any @yield, lock acquisition, timeout, or message wait. By the time your protocol resumes, the tag may have been consumed or the slot may have changed.
Use these rules in protocol code:
- use
querydelete!orquerydelete_wait!when the tag is meant to be consumed; - do not carry a
queryorqueryallresult across a yield and then calluntag!with the potentially outdated tag id; - if you need to lock a slot before acting, acquire the lock and then re-query the slot before deleting the tag or using the result;
- for paired resources, re-check both sides before deleting either side (e.g. in an entanglement swapper that needs to lock two qubits).
query_wait is useful for observing that a matching tag exists. It is going to lock or reserve the tag it returns (or the register in which that tag is).
Filtering By Resource State
Register queries can also filter by slot state:
locked = trueorfalse,assigned = trueorfalse.
That is important in networking protocols because metadata alone is not enough. A slot may carry the right tag but still be unusable because it is already reserved or empty.
Tag Type
QuantumSavory.Tag — Type
Tags are used to represent classical metadata describing the state (or even history) of nodes and their registers. The library allows the construction of custom tags using the Tag constructor. Currently tags are implemented as instances of a sum type and have fairly constrained structure. Most of them are constrained to contain only Symbol instances and integers.
Here is an example of such a generic tag:
julia> Tag(:sometagdescriptor, 1, 2, -3)
SymbolIntIntInt(:sometagdescriptor, 1, 2, -3)::TagA tag can have a custom DataType as first argument, in which case additional customizability in printing is available. E.g. consider the [EntanglementHistory] tag used to track how pairs were entangled before a swap happened.
julia> using QuantumSavory.ProtocolZoo: EntanglementHistory
julia> Tag(EntanglementHistory, 1, 2, 3, 4, 5)
Was entangled to 1.2, but swapped with .5 which was entangled to 3.4The currently supported concrete tag signatures are:
[tuple(m.sig.types[2:end]...) for m in methods(Tag) if m.sig.types[2] ∈ (Symbol, DataType)]24-element Vector{Tuple{DataType, Vararg{DataType}}}:
(Symbol, Float64)
(Symbol, Float64, Float64)
(Symbol, Int64, Int64, Int64, Int64, Int64, Int64)
(Symbol, Int64, Int64, Int64, Int64, Int64)
(Symbol, Int64, Int64, Int64, Int64)
(Symbol, Int64)
(Symbol, Int64, Int64)
(Symbol, Int64, Int64, Int64)
(Symbol,)
(DataType, Symbol, Int64, Int64)
⋮
(DataType, Int64, Int64, Int64, Int64, Int64, Float64)
(DataType, Int64, Int64, Int64, Int64, Float64)
(DataType, Int64, Int64, Int64)
(DataType, Int64, Int64, Int64, Float64)
(DataType, Int64, Int64, Float64)
(DataType, Int64, Int64)
(DataType, Int64, Float64)
(DataType, Int64)
(DataType,)Assigning And Removing Tags
QuantumSavory.tag! — Function
QuantumSavory.untag! — Function
untag!(
ref::Union{RegRef, Register},
id::Integer
) -> @NamedTuple{tag::Tag, slot::Int64, time::Float64}
Remove the tag with the given id from a RegRef or a Register.
To remove a tag based on a query, use querydelete! instead. In asynchronous protocols, do not keep a query result across a yield and later call untag! with the old id. Another process may already have consumed that tag. Re-query under the relevant locks or use a consuming query helper.
See also: querydelete!, query, tag!
Querying
QuantumSavory.query — Function
query(
reg::Union{RegRef, Register},
queryargs::Union{QuantumSavory.Wildcard, Int64, DataType, Function, Symbol}...;
locked,
assigned,
filo
) -> Any
A query function searching for the first slot in a register that has a given tag.
Wildcards are supported (instances of Wildcard also available as the constants W or the emoji ❓ which can be entered as \:question: in the REPL). Predicate functions are also supported (they have to be Int↦Bool functions). The order of query lookup can be specified in terms of FIFO or FILO and defaults to FILO if not specified. The keyword arguments locked and assigned can be used to check, respectively, whether the given slot is locked or whether it contains a quantum state. The keyword argument filo can be used to specify whether the search should be done in a FIFO or FILO order, defaulting to filo=true (i.e. a stack-like behavior).
julia> r = Register(10);
tag!(r[1], :symbol, 2, 3);
tag!(r[2], :symbol, 4, 5);
julia> query(r, :symbol, 4, 5)
(slot = 1043859625813851568.2, id = 4, tag = SymbolIntInt(:symbol, 4, 5)::Tag, time = 0.0)
julia> lock(r[1]);
julia> query(r, :symbol, 4, 5; locked=false) |> isnothing
false
julia> query(r, :symbol, ❓, 3)
(slot = 1043859625813851568.1, id = 3, tag = SymbolIntInt(:symbol, 2, 3)::Tag, time = 0.0)
julia> query(r, :symbol, ❓, 3; assigned=true) |> isnothing
true
julia> query(r, :othersym, ❓, ❓) |> isnothing
true
julia> tag!(r[5], Int, 4, 5);
julia> query(r, Float64, 4, 5) |> isnothing
true
julia> query(r, Int, 4, >(7)) |> isnothing
true
julia> query(r, Int, 4, <(7))
(slot = 1043859625813851568.5, id = 5, tag = TypeIntInt(Int64, 4, 5)::Tag, time = 0.0)A query can be on on a single slot of a register:
julia> r = Register(5);
julia> tag!(r[2], :symbol, 2, 3);
julia> query(r[2], :symbol, 2, 3)
(slot = 2589040728030450388.2, id = 14, tag = SymbolIntInt(:symbol, 2, 3)::Tag, time = 0.0)
julia> query(r[3], :symbol, 2, 3) === nothing
true
julia> queryall(r[2], :symbol, 2, 3)
1-element Vector{@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}}:
(slot = 2589040728030450388.2, id = 14, tag = SymbolIntInt(:symbol, 2, 3)::Tag, time = 0.0)query(
mb::MessageBuffer,
queryargs::Union{QuantumSavory.Wildcard, Int64, DataType, Function, Symbol}...
) -> Union{Nothing, NamedTuple{(:depth, :src, :tag), <:Tuple{Int64, Union{Nothing, Int64}, Any}}}
You are advised to actually use querydelete!, not query when working with classical message buffers.
Wildcards
QuantumSavory.W — Constant
querydelete!
querydelete! is the consuming form of query: it returns the first match and removes it at the same time.
QuantumSavory.querydelete! — Function
querydelete!(
mb::MessageBuffer,
queryargs::Union{QuantumSavory.Wildcard, Int64, DataType, Function, Symbol}...
) -> Union{Nothing, @NamedTuple{src::Union{Nothing, Int64}, tag::T} where T}
A query for classical message buffers that also deletes the message out of the buffer.
julia> net = RegisterNet([Register(3), Register(2)])
A network of 2 registers in a graph of 1 edges
julia> put!(channel(net, 1=>2), Tag(:my_tag));
julia> put!(channel(net, 1=>2), Tag(:another_tag, 123, 456));
julia> query(messagebuffer(net, 2), :my_tag)
julia> run(get_time_tracker(net))
julia> query(messagebuffer(net, 2), :my_tag)
(depth = 1, src = 1, tag = Symbol(:my_tag)::Tag)
julia> querydelete!(messagebuffer(net, 2), :my_tag)
@NamedTuple{src::Union{Nothing, Int64}, tag::Tag}((1, Symbol(:my_tag)::Tag))
julia> querydelete!(messagebuffer(net, 2), :my_tag) === nothing
true
julia> querydelete!(messagebuffer(net, 2), :another_tag, ❓, ❓)
@NamedTuple{src::Union{Nothing, Int64}, tag::Tag}((1, SymbolIntInt(:another_tag, 123, 456)::Tag))
julia> querydelete!(messagebuffer(net, 2), :another_tag, ❓, ❓) === nothing
trueYou can also wait on a message buffer for a message to arrive before running a query:
julia> using ResumableFunctions; using ConcurrentSim;
julia> net = RegisterNet([Register(3), Register(2), Register(3)])
A network of 3 registers in a graph of 2 edges
julia> env = get_time_tracker(net);
julia> @resumable function receive_tags(env)
while true
mb = messagebuffer(net, 2)
@yield onchange(mb)
msg = querydelete!(mb, :second_tag, ❓, ❓)
print("t=$(now(env)): query returns ")
if isnothing(msg)
println("nothing")
else
println("$(msg.tag) received from node $(msg.src)")
end
end
end
receive_tags (generic function with 1 method)
julia> @resumable function send_tags(env)
@yield timeout(env, 1.0)
put!(channel(net, 1=>2), Tag(:my_tag))
@yield timeout(env, 2.0)
put!(channel(net, 3=>2), Tag(:second_tag, 123, 456))
end
send_tags (generic function with 1 method)
julia> @process send_tags(env);
julia> @process receive_tags(env);
julia> run(env, 10)
t=1.0: query returns nothing
t=3.0: query returns SymbolIntInt(:second_tag, 123, 456)::Tag received from node 3querydelete!(
reg::Union{RegRef, Register},
args...;
kwa...
) -> Any
A query for Register or a register slot (i.e. a RegRef) that also deletes the tag.
For register protocol code, this is safer than query followed by untag!. If the result will be used after an @yield or lock acquisition, re-query after the wait before deleting or acting on the tag.
julia> reg = Register(3)
tag!(reg[1], :tagA, 1, 2, 3)
tag!(reg[2], :tagA, 10, 20, 30)
tag!(reg[2], :tagB, 6, 7, 8);
julia> queryall(reg, :tagA, ❓, ❓, ❓)
2-element Vector{@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}}:
(slot = 767672459337976635.2, id = 19, tag = SymbolIntIntInt(:tagA, 10, 20, 30)::Tag, time = 0.0)
(slot = 767672459337976635.1, id = 18, tag = SymbolIntIntInt(:tagA, 1, 2, 3)::Tag, time = 0.0)
julia> querydelete!(reg, :tagA, ❓, ❓, ❓)
(slot = 767672459337976635.2, id = 19, tag = SymbolIntIntInt(:tagA, 10, 20, 30)::Tag, time = 0.0)
julia> queryall(reg, :tagA, ❓, ❓, ❓)
1-element Vector{@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}}:
(slot = 767672459337976635.1, id = 18, tag = SymbolIntIntInt(:tagA, 1, 2, 3)::Tag, time = 0.0)queryall
queryall returns every matching register tag.
QuantumSavory.queryall — Function
queryall(
reg::Union{RegRef, Register},
queryargs::Union{QuantumSavory.Wildcard, Int64, DataType, Function, Symbol}...;
filo,
kwargs...
) -> Any
A query function that returns all slots of a register that have a given tag, with support for predicates and wildcards.
julia> r = Register(10);
tag!(r[1], :symbol, 2, 3);
tag!(r[2], :symbol, 4, 5);
julia> queryall(r, :symbol, ❓, ❓)
2-element Vector{@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}}:
(slot = 15531193455478883312.2, id = 16, tag = SymbolIntInt(:symbol, 4, 5)::Tag, time = 0.0)
(slot = 15531193455478883312.1, id = 15, tag = SymbolIntInt(:symbol, 2, 3)::Tag, time = 0.0)
julia> queryall(r, :symbol, ❓, >(4))
1-element Vector{@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}}:
(slot = 15531193455478883312.2, id = 16, tag = SymbolIntInt(:symbol, 4, 5)::Tag, time = 0.0)
julia> queryall(r, :symbol, ❓, >(5))
@NamedTuple{slot::RegRef, id::Int128, tag::Tag, time::Float64}[]Where To Go Next
- Read Discrete Event Simulator for
query_waitandquerydelete_wait!. - Read Backend Simulators and Register Interface API for the quantum side of the same workflow.