Defines the metadata schema for SystemVerilog ports exposed to Elixir.
The C++ wrapper emits one signal specification for each Verilated top-level
port in the metadata command response. The schema is JSON-compatible and
therefore uses string keys in its canonical form:
%{
"name" => "count",
"direction" => "output",
"type" => "logic",
"width" => 8,
"signed" => false,
"packed" => %{
"kind" => "packed_vector",
"dimensions" => [%{"left" => 7, "right" => 0}]
},
"role" => %{"kind" => "data"}
}Required fields:
"name"- a simple SystemVerilog identifier exposed through the runtime protocol."direction"- one of"input","output", or"inout"."type"- the supported base type, currently"bit"or"logic"."width"- the canonical runtime width in bits."signed"- whether the value has a signed numeric interpretation."packed"- scalar metadata or one canonical packed-vector dimension."role"-"data","clock", or"reset"metadata.
The wrapper must canonicalise every supported vector as [width - 1:0] before
emitting metadata. Elixir consumes the list with normalize_many/1, finds
signals with lookup/2, and checks command legality with validate_poke/2
and validate_peek/1 before sending runtime requests.
Unsupported or ambiguous shapes are rejected rather than guessed. This MVP excludes escaped identifiers, unpacked arrays, multi-dimensional packed arrays, structs, unions, interfaces, real-valued ports, implicit widths, and non-canonical packed ranges.
Examples
The schema is versioned so the wrapper and Elixir side can negotiate future changes explicitly.
iex> SvPortSim.SignalSpec.schema_version()
1A small module with a clock, active-low reset, scalar input, output vector, and signed input vector can be represented as JSON-compatible maps.
iex> specs = SvPortSim.SignalSpec.example_specs()
iex> Enum.map(specs, & &1["name"])
["clk", "rst_n", "enable", "count", "delta"]
iex> SvPortSim.SignalSpec.validate_many(specs)
:okClock and reset roles are explicit.
iex> {:ok, clk} = SvPortSim.SignalSpec.lookup(SvPortSim.SignalSpec.example_specs(), "clk")
iex> {clk["role"]["kind"], clk["role"]["edge"]}
{"clock", "posedge"}
iex> {:ok, rst} = SvPortSim.SignalSpec.lookup(SvPortSim.SignalSpec.example_specs(), "rst_n")
iex> {rst["role"]["kind"], rst["role"]["active"]}
{"reset", "low"}Direction metadata is sufficient for the Elixir side to reject illegal pokes and peeks before the request is sent to the wrapper.
iex> enable = SvPortSim.SignalSpec.data("enable", "input", "bit", 1)
iex> SvPortSim.SignalSpec.validate_poke(enable, %{"bits" => "1", "width" => 1})
:ok
iex> SvPortSim.SignalSpec.validate_peek(enable)
{:error, {:not_readable, "enable", "input"}}
iex> count = SvPortSim.SignalSpec.data("count", "output", "logic", 8)
iex> SvPortSim.SignalSpec.validate_peek(count)
:ok
iex> SvPortSim.SignalSpec.validate_poke(count, %{"bits" => "00000000", "width" => 8})
{:error, {:not_writable, "count", "output"}}Non-canonical packed-vector ranges are rejected because bit ordering would be ambiguous for runtime values.
iex> spec = SvPortSim.SignalSpec.data("count", "output", "logic", 8)
iex> spec = put_in(spec, ["packed", "dimensions"], [%{"left" => 0, "right" => 7}])
iex> SvPortSim.SignalSpec.validate(spec)
{:error, {:unsupported_packed_range, %{"left" => 0, "right" => 7}, :canonical_range_required}}
Summary
Functions
Builds a clock signal specification.
Returns the accepted clock edges.
Builds a data-role signal specification.
Returns the accepted port directions.
Returns example metadata for a small module.
Looks up a validated signal by name.
Returns the maximum supported signal width.
Normalises and validates one signal specification.
Normalises and validates a list of signal specifications.
Returns whether a signal can be read by a peek command.
Builds a reset signal specification.
Returns the accepted reset active levels.
Returns the accepted signal role kinds.
Returns a machine-readable summary of the schema.
Returns the SignalSpec schema version.
Returns the runtime data-type descriptor for a signal.
Returns the accepted base types.
Returns explicitly unsupported signal shapes for the MVP contract.
Validates one signal specification.
Validates a list of signal specifications and rejects duplicate names.
Validates that a peek request is legal for a signal.
Validates that a poke request is legal for a signal and encoded value.
Returns whether a signal can be written by a poke command.
Types
Functions
Builds a clock signal specification.
Clocks are scalar input signals. The :edge option defaults to "posedge".
Examples
iex> spec = SvPortSim.SignalSpec.clock("clk")
iex> {spec["name"], spec["direction"], spec["width"], spec["role"]["kind"], spec["role"]["edge"]}
{"clk", "input", 1, "clock", "posedge"}
@spec clock_edges() :: [String.t()]
Returns the accepted clock edges.
Examples
iex> SvPortSim.SignalSpec.clock_edges()
["posedge", "negedge"]
Builds a data-role signal specification.
The returned map is canonical when the arguments are valid. Use validate/1
when arguments may have come from an external source.
Examples
iex> spec = SvPortSim.SignalSpec.data("count", "output", "logic", 8)
iex> [dimension] = spec["packed"]["dimensions"]
iex> {spec["name"], spec["direction"], spec["type"], spec["width"], dimension["left"], dimension["right"]}
{"count", "output", "logic", 8, 7, 0}
@spec directions() :: [direction()]
Returns the accepted port directions.
Examples
iex> SvPortSim.SignalSpec.directions()
["input", "output", "inout"]
@spec example_specs() :: [t()]
Returns example metadata for a small module.
The example models this port list:
module Counter(
input bit clk,
input bit rst_n,
input bit enable,
output logic [7:0] count,
input logic signed [3:0] delta
);Examples
iex> specs = SvPortSim.SignalSpec.example_specs()
iex> Enum.map(specs, & &1["name"])
["clk", "rst_n", "enable", "count", "delta"]
iex> SvPortSim.SignalSpec.validate_many(specs)
:ok
Looks up a validated signal by name.
Examples
iex> {:ok, spec} = SvPortSim.SignalSpec.lookup(SvPortSim.SignalSpec.example_specs(), "count")
iex> {spec["direction"], spec["width"]}
{"output", 8}
iex> SvPortSim.SignalSpec.lookup(SvPortSim.SignalSpec.example_specs(), "missing")
{:error, {:unknown_signal, "missing"}}
@spec max_width() :: pos_integer()
Returns the maximum supported signal width.
Examples
iex> SvPortSim.SignalSpec.max_width()
4096
Normalises and validates one signal specification.
Atom keys and atom enum values are accepted as input convenience and converted to canonical string keys and values.
Examples
iex> {:ok, spec} = SvPortSim.SignalSpec.normalize(%{
...> name: "enable",
...> direction: :input,
...> type: :bit,
...> width: 1,
...> signed: false,
...> packed: %{kind: :scalar, dimensions: []},
...> role: %{kind: :data}
...> })
iex> spec["direction"]
"input"
Normalises and validates a list of signal specifications.
Examples
iex> {:ok, specs} = SvPortSim.SignalSpec.normalize_many(SvPortSim.SignalSpec.example_specs())
iex> length(specs)
5
Returns whether a signal can be read by a peek command.
Output and inout ports are readable.
Examples
iex> SvPortSim.SignalSpec.readable?(SvPortSim.SignalSpec.data("count", "output", "logic", 8))
true
iex> SvPortSim.SignalSpec.readable?(SvPortSim.SignalSpec.data("enable", "input", "bit", 1))
false
Builds a reset signal specification.
Resets are scalar input signals. The :active option defaults to "high".
Examples
iex> spec = SvPortSim.SignalSpec.reset("rst_n", active: "low")
iex> {spec["name"], spec["direction"], spec["width"], spec["role"]["kind"], spec["role"]["active"]}
{"rst_n", "input", 1, "reset", "low"}
@spec reset_active_levels() :: [String.t()]
Returns the accepted reset active levels.
Examples
iex> SvPortSim.SignalSpec.reset_active_levels()
["high", "low"]
@spec role_kinds() :: [role_kind()]
Returns the accepted signal role kinds.
Examples
iex> SvPortSim.SignalSpec.role_kinds()
["data", "clock", "reset"]
@spec schema() :: map()
Returns a machine-readable summary of the schema.
Examples
iex> schema = SvPortSim.SignalSpec.schema()
iex> schema["directions"]
["input", "output", "inout"]
iex> schema["roles"]
["data", "clock", "reset"]
@spec schema_version() :: pos_integer()
Returns the SignalSpec schema version.
Examples
iex> SvPortSim.SignalSpec.schema_version()
1
@spec type_descriptor(term()) :: {:ok, SvPortSim.Protocol.DataType.t()} | {:error, term()}
Returns the runtime data-type descriptor for a signal.
The descriptor is delegated to SvPortSim.Protocol.DataType so signal metadata
and runtime value encoding stay aligned.
Examples
iex> {:ok, type} = SvPortSim.SignalSpec.type_descriptor(SvPortSim.SignalSpec.data("count", "output", "logic", 8))
iex> {type.kind, type.base, type.width, type.role}
{:vector, :logic, 8, :data}
iex> {:ok, type} = SvPortSim.SignalSpec.type_descriptor(SvPortSim.SignalSpec.reset("rst_n", active: "low"))
iex> {type.role, type.active}
{:reset, :low}
@spec types() :: [base_type()]
Returns the accepted base types.
Examples
iex> SvPortSim.SignalSpec.types()
["bit", "logic"]
@spec unsupported_features() :: [atom()]
Returns explicitly unsupported signal shapes for the MVP contract.
Examples
iex> :unpacked_arrays in SvPortSim.SignalSpec.unsupported_features()
true
iex> :non_canonical_packed_ranges in SvPortSim.SignalSpec.unsupported_features()
true
Validates one signal specification.
Examples
iex> SvPortSim.SignalSpec.validate(SvPortSim.SignalSpec.clock("clk"))
:ok
iex> SvPortSim.SignalSpec.validate(%{})
{:error, {:missing_fields, "signal", ["direction", "name", "packed", "role", "signed", "type", "width"]}}
Validates a list of signal specifications and rejects duplicate names.
Examples
iex> specs = [
...> SvPortSim.SignalSpec.data("enable", "input", "bit", 1),
...> SvPortSim.SignalSpec.data("enable", "output", "logic", 1)
...> ]
iex> SvPortSim.SignalSpec.validate_many(specs)
{:error, {:duplicate_signal_names, ["enable"]}}
Validates that a peek request is legal for a signal.
Examples
iex> count = SvPortSim.SignalSpec.data("count", "output", "logic", 8)
iex> SvPortSim.SignalSpec.validate_peek(count)
:ok
iex> enable = SvPortSim.SignalSpec.data("enable", "input", "bit", 1)
iex> SvPortSim.SignalSpec.validate_peek(enable)
{:error, {:not_readable, "enable", "input"}}
Validates that a poke request is legal for a signal and encoded value.
Examples
iex> enable = SvPortSim.SignalSpec.data("enable", "input", "bit", 1)
iex> SvPortSim.SignalSpec.validate_poke(enable, %{"bits" => "1", "width" => 1})
:ok
iex> SvPortSim.SignalSpec.validate_poke(enable, %{"bits" => "x", "width" => 1})
{:error, {:invalid_bits, "x", ["0", "1"]}}
Returns whether a signal can be written by a poke command.
Input and inout ports are writable.
Examples
iex> SvPortSim.SignalSpec.writable?(SvPortSim.SignalSpec.data("enable", "input", "bit", 1))
true
iex> SvPortSim.SignalSpec.writable?(SvPortSim.SignalSpec.data("count", "output", "logic", 8))
false