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 RegRef slots, added with tag!, which describe quantum resources;
  • messages in a MessageBuffer, inserted with put!, 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, and time;
  • querying a message buffer returns the matching src and tag, plus an internal depth used 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) or Tag(: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:

  • query returns the first match;
  • queryall returns all matches;
  • querydelete! returns one match and removes it.

For message buffers:

  • query is available, but querydelete! 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! or querydelete_wait! when the tag is meant to be consumed;
  • do not carry a query or queryall result across a yield and then call untag! 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 = true or false,
  • assigned = true or false.

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.TagType

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)::Tag

A 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.4

See also: tag!, query

source

The 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.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!

source

Querying

QuantumSavory.queryFunction
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 IntBool 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)

See also: queryall, tag!, W,

source
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.

source

Wildcards

QuantumSavory.❓Constant

A wildcard instance for use with the tag querying functionality.

This emoji can be inputted with the \:question: emoji shortcut, or you can simply use the ASCII alternative W.

See also: query, tag!, W

source

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
true

You 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 3
source
querydelete!(
    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)
source

queryall

queryall returns every matching register tag.

QuantumSavory.queryallFunction
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}[]
source

Where To Go Next