SvPortSim.SignalSpec (SvPortSim v0.1.0)

Copy Markdown View Source

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()
1

A 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)
:ok

Clock 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

base_type()

@type base_type() :: String.t()

direction()

@type direction() :: String.t()

role_kind()

@type role_kind() :: String.t()

t()

@type t() :: %{required(String.t()) => term()}

Functions

clock(name, opts \\ [])

@spec clock(
  term(),
  keyword()
) :: t()

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"}

clock_edges()

@spec clock_edges() :: [String.t()]

Returns the accepted clock edges.

Examples

iex> SvPortSim.SignalSpec.clock_edges()
["posedge", "negedge"]

data(name, direction, type, width, opts \\ [])

@spec data(term(), term(), term(), term(), keyword()) :: t()

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}

directions()

@spec directions() :: [direction()]

Returns the accepted port directions.

Examples

iex> SvPortSim.SignalSpec.directions()
["input", "output", "inout"]

example_specs()

@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

lookup(specs, name)

@spec lookup(term(), term()) :: {:ok, t()} | {:error, term()}

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"}}

max_width()

@spec max_width() :: pos_integer()

Returns the maximum supported signal width.

Examples

iex> SvPortSim.SignalSpec.max_width()
4096

normalize(spec)

@spec normalize(term()) :: {:ok, t()} | {:error, term()}

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"

normalize_many(specs)

@spec normalize_many(term()) :: {:ok, [t()]} | {:error, term()}

Normalises and validates a list of signal specifications.

Examples

iex> {:ok, specs} = SvPortSim.SignalSpec.normalize_many(SvPortSim.SignalSpec.example_specs())
iex> length(specs)
5

readable?(spec)

@spec readable?(term()) :: boolean()

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

reset(name, opts \\ [])

@spec reset(
  term(),
  keyword()
) :: t()

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"}

reset_active_levels()

@spec reset_active_levels() :: [String.t()]

Returns the accepted reset active levels.

Examples

iex> SvPortSim.SignalSpec.reset_active_levels()
["high", "low"]

role_kinds()

@spec role_kinds() :: [role_kind()]

Returns the accepted signal role kinds.

Examples

iex> SvPortSim.SignalSpec.role_kinds()
["data", "clock", "reset"]

schema()

@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"]

schema_version()

@spec schema_version() :: pos_integer()

Returns the SignalSpec schema version.

Examples

iex> SvPortSim.SignalSpec.schema_version()
1

type_descriptor(spec)

@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}

types()

@spec types() :: [base_type()]

Returns the accepted base types.

Examples

iex> SvPortSim.SignalSpec.types()
["bit", "logic"]

unsupported_features()

@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

validate(spec)

@spec validate(term()) :: :ok | {:error, term()}

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"]}}

validate_many(specs)

@spec validate_many(term()) :: :ok | {:error, term()}

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"]}}

validate_peek(spec)

@spec validate_peek(term()) :: :ok | {:error, term()}

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"}}

validate_poke(spec, encoded_value)

@spec validate_poke(term(), term()) :: :ok | {:error, term()}

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"]}}

writable?(spec)

@spec writable?(term()) :: boolean()

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