This commit is contained in:
Jacob Lifshay 2025-12-03 21:27:26 -08:00
parent dca59a24ec
commit b6f3ecfb32
Signed by: programmerjake
SSH key fingerprint: SHA256:HnFTLGpSm4Q4Fj502oCFisjZSoakwEuTsJJMSke63RQ
6 changed files with 606 additions and 17 deletions

7
Cargo.lock generated
View file

@ -211,6 +211,7 @@ version = "0.1.0"
dependencies = [
"fayalite",
"serde",
"simple-mermaid",
]
[[package]]
@ -690,6 +691,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "simple-mermaid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589144a964b4b30fe3a83b4bb1a09e2475aac194ec832a046a23e75bddf9eb29"
[[package]]
name = "strsim"
version = "0.11.1"

View file

@ -16,6 +16,7 @@ rust-version = "1.89.0"
[workspace.dependencies]
fayalite = { git = "https://git.libre-chip.org/libre-chip/fayalite.git", version = "0.3.0", branch = "master" }
serde = { version = "1.0.202", features = ["derive"] }
simple-mermaid = "0.2.0"
[profile.dev]
opt-level = 1

View file

@ -17,3 +17,4 @@ version.workspace = true
[dependencies]
fayalite.workspace = true
serde.workspace = true
simple-mermaid.workspace = true

View file

@ -8,6 +8,8 @@
//! after the `decode` stage there's a `post_decode` stage (that may run in the same clock cycle as `decode`)
//! that checks that the fetched instructions' kinds match the predicted instruction kinds and that feeds
//! information back to the `fetch` stage to cancel fetches that need to be predicted differently.
//!
#![doc = simple_mermaid::mermaid!("next_pc/next_pc.mermaid")]
use crate::{
config::{CpuConfig, CpuConfigFetchWidth},
@ -17,6 +19,7 @@ use fayalite::{
int::{UIntInRange, UIntInRangeInclusive, UIntInRangeType},
prelude::*,
sim::value::SimOnlyValueTrait,
ty::StaticType,
util::ready_valid::ReadyValid,
};
@ -142,6 +145,505 @@ pub struct PostDecodeOutputInterface<C: PhantomConstGet<CpuConfig>> {
pub config: C,
}
#[hdl(no_static)]
struct Cancel<C: PhantomConstGet<CpuConfig>> {
call_stack: CallStack,
start_pc: UInt<64>,
new_btb_entry: HdlOption<BTBEntryWithoutStartPc>,
btb_entry_index: HdlOption<UIntInRange<0, { BranchTargetBuffer::SIZE }>>,
config: C,
}
/// the output of `Stage::run`.
/// when cancelling operations, the returned [`StageOutput.cancel`] should be the state after running all operations returned in [`StageOutput.output`]
#[hdl(no_static)]
struct StageOutput<Output, MaxOutputCount: Size, C: PhantomConstGet<CpuConfig>> {
outputs: ArrayVec<Output, MaxOutputCount>,
cancel: HdlOption<Cancel<C>>,
}
trait Stage: Type + SimValueDefault + ResetSteps {
type Inputs: Type;
type Output: Type;
type MaxOutputCount: Size;
fn output_ty(config: PhantomConst<CpuConfig>) -> Self::Output;
fn max_output_count(
config: PhantomConst<CpuConfig>,
) -> <Self::MaxOutputCount as Size>::SizeType;
fn stage_output_ty(
config: PhantomConst<CpuConfig>,
) -> StageOutput<Self::Output, Self::MaxOutputCount, PhantomConst<CpuConfig>> {
StageOutput[Self::output_ty(config)][Self::max_output_count(config)][config]
}
fn run(
state: &mut SimValue<Self>,
inputs: &SimValue<Self::Inputs>,
) -> SimValue<StageOutput<Self::Output, Self::MaxOutputCount, PhantomConst<CpuConfig>>>;
/// changes state to match `cancel`
fn cancel(state: &mut SimValue<Self>, cancel: &SimValue<Cancel<PhantomConst<CpuConfig>>>);
}
#[hdl(no_static)]
struct NextPcStageOutput<C: PhantomConstGet<CpuConfig>> {
start_pc: UInt<64>,
next_start_pc: UInt<64>,
btb_entry: HdlOption<(
UIntInRange<0, { BranchTargetBuffer::SIZE }>,
BTBEntryWithoutStartPc,
)>,
fetch_block_id: UInt<{ FETCH_BLOCK_ID_WIDTH }>,
config: C,
}
#[hdl(no_static)]
struct NextPcStageState<C: PhantomConstGet<CpuConfig>> {
call_stack: CallStack,
branch_target_buffer: BranchTargetBuffer,
next_pc: UInt<64>,
next_fetch_block_id: UInt<{ FETCH_BLOCK_ID_WIDTH }>,
config: C,
}
impl SimValueDefault for NextPcStageState<PhantomConst<CpuConfig>> {
#[hdl]
fn sim_value_default(self) -> SimValue<Self> {
let Self {
call_stack,
branch_target_buffer,
next_pc: _,
next_fetch_block_id: _,
config,
} = self;
#[hdl(sim)]
Self {
call_stack: call_stack.sim_value_default(),
branch_target_buffer: branch_target_buffer.sim_value_default(),
// use something other than the default so you can see the reset progress
next_pc: !0u64,
// use something other than the default so you can see the reset progress
next_fetch_block_id: !0u8,
config,
}
}
}
impl ResetSteps for NextPcStageState<PhantomConst<CpuConfig>> {
#[hdl]
fn reset_step(this: &mut SimValue<Self>, step: usize) -> ResetStatus {
#[hdl(sim)]
let Self {
call_stack,
branch_target_buffer,
next_pc,
next_fetch_block_id,
config: _,
} = this;
**next_pc = 0u64.into(); // match Microwatt's reset PC
**next_fetch_block_id = 0u8.into();
let call_stack = ResetSteps::reset_step(call_stack, step);
let branch_target_buffer = ResetSteps::reset_step(branch_target_buffer, step);
call_stack.and(branch_target_buffer)
}
}
impl Stage for NextPcStageState<PhantomConst<CpuConfig>> {
type Inputs = ();
type Output = NextPcStageOutput<PhantomConst<CpuConfig>>;
type MaxOutputCount = ConstUsize<1>;
fn output_ty(config: PhantomConst<CpuConfig>) -> Self::Output {
NextPcStageOutput[config]
}
fn max_output_count(
_config: PhantomConst<CpuConfig>,
) -> <Self::MaxOutputCount as Size>::SizeType {
ConstUsize
}
#[hdl]
fn run(
state: &mut SimValue<Self>,
_inputs: &SimValue<Self::Inputs>,
) -> SimValue<StageOutput<Self::Output, Self::MaxOutputCount, PhantomConst<CpuConfig>>> {
let config = state.config.ty();
let fetch_block_id = state.next_fetch_block_id.as_int();
*state.next_fetch_block_id = state.next_fetch_block_id.as_int().wrapping_add(1).into();
let start_pc = state.next_pc.as_int();
let fetch_pc = start_pc & (!0u64 << config.get().log2_fetch_width_in_bytes);
let btb_entry_index = state
.branch_target_buffer
.branch_pc_to_target_map
.iter()
.position(|entry| {
#[hdl(sim)]
match entry {
HdlNone => false,
HdlSome(entry) => entry.start_pc.as_int() == start_pc,
}
});
let (next_start_pc, btb_entry) = if let Some(btb_entry_index) = btb_entry_index {
#[hdl(sim)]
let Self {
call_stack,
branch_target_buffer,
..
} = state;
let entry = #[hdl(sim)]
match &branch_target_buffer.branch_pc_to_target_map[btb_entry_index] {
HdlSome(entry) => entry,
_ => unreachable!(),
};
let next_start_pc = #[hdl(sim)]
match &entry.rest.insn_kind {
BTBEntryInsnKind::Branch => {
if BTBEntryAddrKind::taken(&entry.rest.addr_kind) {
BTBEntry::taken_pc(entry)
} else {
BTBEntry::not_taken_start_pc(entry)
}
}
BTBEntryInsnKind::Call => {
if BTBEntryAddrKind::taken(&entry.rest.addr_kind) {
CallStack::push(call_stack, BTBEntry::after_call_pc(entry));
BTBEntry::taken_pc(entry)
} else {
BTBEntry::not_taken_start_pc(entry)
}
}
BTBEntryInsnKind::Ret => {
if BTBEntryAddrKind::taken(&entry.rest.addr_kind) {
CallStack::pop(call_stack).unwrap_or(BTBEntry::taken_pc(entry))
} else {
BTBEntry::not_taken_start_pc(entry)
}
}
BTBEntryInsnKind::Unknown => unreachable!(),
};
(
next_start_pc,
#[hdl(sim)]
HdlSome((btb_entry_index, &entry.rest)),
)
} else {
(
fetch_pc.wrapping_add(config.get().fetch_width_in_bytes() as u64),
#[hdl(sim)]
HdlNone(),
)
};
let output = #[hdl(sim)]
NextPcStageOutput::<_> {
start_pc,
next_start_pc,
btb_entry,
fetch_block_id,
config,
};
#[hdl(sim)]
StageOutput::<_, _, _> {
outputs: Self::stage_output_ty(config).outputs.new_full_sim([output]),
cancel: #[hdl(sim)]
(HdlOption[Cancel[config]]).HdlNone(),
}
}
#[hdl]
fn cancel(state: &mut SimValue<Self>, cancel: &SimValue<Cancel<PhantomConst<CpuConfig>>>) {
#[hdl(sim)]
let Self {
call_stack,
branch_target_buffer,
next_pc,
next_fetch_block_id: _,
config: _,
} = state;
#[hdl(sim)]
let Cancel::<_> {
call_stack: new_call_stack,
start_pc,
new_btb_entry,
btb_entry_index,
config: _,
} = cancel;
call_stack.clone_from(new_call_stack);
next_pc.clone_from(start_pc);
#[hdl(sim)]
if let HdlSome(new_btb_entry) = new_btb_entry {
// add/update btb entry
// get old entry if it's still there
let btb_entry_index = #[hdl(sim)]
if let HdlSome(btb_entry_index) = btb_entry_index {
#[hdl(sim)]
if let HdlSome(entry) =
&branch_target_buffer.branch_pc_to_target_map[**btb_entry_index]
{
if entry.start_pc == *start_pc {
// found the old entry
Some(**btb_entry_index)
} else {
None
}
} else {
None
}
} else {
None
};
let btb_entry_index = btb_entry_index.unwrap_or_else(|| {
// old entry isn't there, pick an entry to replace
BranchTargetBuffer::next_index_to_replace(branch_target_buffer)
});
// replace with new entry
branch_target_buffer.branch_pc_to_target_map[btb_entry_index] = #[hdl(sim)]
HdlSome(
#[hdl(sim)]
BTBEntry {
start_pc,
rest: new_btb_entry,
},
);
} else if let HdlSome(btb_entry_index) = btb_entry_index {
// remove btb entry if it's still there
let entry_mut = &mut branch_target_buffer.branch_pc_to_target_map[**btb_entry_index];
#[hdl(sim)]
if let HdlSome(entry) = &entry_mut {
if entry.start_pc == *start_pc {
// we found it, remove it
*entry_mut = #[hdl(sim)]
HdlNone();
}
}
}
}
}
#[hdl(no_static)]
struct BrPredStageOutput<C: PhantomConstGet<CpuConfig>> {
config: C,
}
impl SimValueDefault for BrPredStageOutput<PhantomConst<CpuConfig>> {
#[hdl]
fn sim_value_default(self) -> SimValue<Self> {
#[hdl(sim)]
Self {
config: self.config,
}
}
}
#[hdl(no_static)]
struct BrPredStageState<C: PhantomConstGet<CpuConfig>> {
branch_history: UInt<6>,
branch_predictor: Array<BranchPredictionState, { BRANCH_PREDICTOR_SIZE }>,
config: C,
}
impl BrPredStageState<PhantomConst<CpuConfig>> {
fn branch_predictor_index(this: &SimValue<Self>, branch_pc: u64) -> usize {
let mut t = this.branch_history.cast_to_static::<UInt<64>>().as_int();
t ^= t.rotate_left(5) & !branch_pc.rotate_right(3);
t ^= branch_pc;
t ^= !t.rotate_left(2) & t.rotate_left(4);
let mut retval = 0;
for i in (0..BRANCH_PREDICTOR_LOG2_SIZE).step_by(BRANCH_PREDICTOR_LOG2_SIZE) {
retval ^= t >> i;
}
retval as usize % BRANCH_PREDICTOR_SIZE
}
}
impl SimValueDefault for BrPredStageState<PhantomConst<CpuConfig>> {
#[hdl]
fn sim_value_default(self) -> SimValue<Self> {
let Self {
branch_history: _,
branch_predictor: _,
config,
} = self;
#[hdl(sim)]
Self {
// use something other than the default so you can see the reset progress
branch_history: (-1i8).cast_to_static::<UInt<_>>(),
// use something other than the default so you can see the reset progress
branch_predictor: std::array::from_fn(|_| {
BranchPredictionState::towards_not_taken(&BranchPredictionState.sim_value_default())
}),
config,
}
}
}
impl ResetSteps for BrPredStageState<PhantomConst<CpuConfig>> {
#[hdl]
fn reset_step(this: &mut SimValue<Self>, step: usize) -> ResetStatus {
#[hdl(sim)]
let Self {
branch_history,
branch_predictor,
config: _,
} = this;
**branch_history = 0u8.cast_to_static::<UInt<_>>();
ResetSteps::reset_step(branch_predictor, step)
}
}
impl Stage for BrPredStageState<PhantomConst<CpuConfig>> {
type Inputs = NextPcStageOutput<PhantomConst<CpuConfig>>;
type Output = BrPredStageOutput<PhantomConst<CpuConfig>>;
type MaxOutputCount = ConstUsize<1>;
fn output_ty(config: PhantomConst<CpuConfig>) -> Self::Output {
BrPredStageOutput[config]
}
fn max_output_count(
_config: PhantomConst<CpuConfig>,
) -> <Self::MaxOutputCount as Size>::SizeType {
ConstUsize
}
#[hdl]
fn run(
state: &mut SimValue<Self>,
inputs: &SimValue<Self::Inputs>,
) -> SimValue<StageOutput<Self::Output, Self::MaxOutputCount, PhantomConst<CpuConfig>>> {
let config = state.config.ty();
#[hdl(sim)]
let NextPcStageOutput::<_> {
start_pc,
next_start_pc,
btb_entry,
fetch_block_id,
config: _,
} = inputs;
#[hdl(sim)]
if let HdlSome(btb_entry) = btb_entry {
let taken = #[hdl(sim)]
match &btb_entry.1.addr_kind {
BTBEntryAddrKind::Unconditional | BTBEntryAddrKind::Indirect => None,
BTBEntryAddrKind::CondTaken => Some(true),
BTBEntryAddrKind::CondNotTaken => Some(false),
};
if let Some(taken) = taken {
let index = Self::branch_predictor_index(
state,
BTBEntry::branch_pc(
&#[hdl(sim)]
BTBEntry {
start_pc,
rest: &btb_entry.1,
},
),
);
if taken != BranchPredictionState::is_taken(&state.branch_predictor[index]) {
let retval = #[hdl(sim)]
StageOutput::<_, _, _> {
outputs: Self::stage_output_ty(config).outputs.sim_value_default(),
cancel: #[hdl(sim)]
(HdlOption[Cancel[config]]).HdlSome(
#[hdl(sim)]
Cancel::<_> {
call_stack: todo!(),
start_pc: todo!(),
new_btb_entry: todo!(),
btb_entry_index: todo!(),
config: todo!(),
},
),
};
return retval;
}
}
}
let output = #[hdl(sim)]
BrPredStageOutput::<_> { config };
#[hdl(sim)]
StageOutput::<_, _, _> {
outputs: Self::stage_output_ty(config).outputs.new_full_sim([output]),
cancel: #[hdl(sim)]
(HdlOption[Cancel[config]]).HdlNone(),
}
}
fn cancel(state: &mut SimValue<Self>, cancel: &SimValue<Cancel<PhantomConst<CpuConfig>>>) {
todo!()
}
}
#[hdl(no_static)]
struct FetchOpInFetchDecode<C: PhantomConstGet<CpuConfig>> {
start_pc: UInt<64>,
config: C,
}
impl<C: Type + PhantomConstGet<CpuConfig>> FetchOpInFetchDecode<C> {
#[hdl]
fn from_fetch_op_in_next_pc(v: SimValue<FetchOpInNextPc<C>>) -> SimValue<Self> {
#[hdl(sim)]
let FetchOpInNextPc::<_> { start_pc, config } = v;
#[hdl(sim)]
Self { start_pc, config }
}
}
#[hdl(no_static)]
struct InsnInPostDecode<C: PhantomConstGet<CpuConfig>> {
insn: WipDecodedInsn,
config: C,
}
impl<C: Type + PhantomConstGet<CpuConfig>> InsnInPostDecode<C> {
#[hdl]
fn from_fetch_op_in_fetch_decode(
fetch_op: SimValue<FetchOpInFetchDecode<C>>,
insn: SimValue<WipDecodedInsn>,
) -> SimValue<InsnInPostDecode<C>> {
#[hdl(sim)]
let FetchOpInFetchDecode::<_> {} = fetch_op;
}
}
#[hdl(no_static)]
struct InsnInRenameDispatchExecute<C: PhantomConstGet<CpuConfig>> {
insn: WipDecodedInsn,
config: C,
}
impl<C: Type + PhantomConstGet<CpuConfig>> InsnInRenameDispatchExecute<C> {
#[hdl]
fn from_insn_in_post_decode(v: SimValue<InsnInPostDecode<C>>) -> SimValue<Self> {
#[hdl(sim)]
let InsnInPostDecode::<_> { insn, config } = v;
#[hdl(sim)]
Self { insn, config }
}
}
#[hdl(no_static)]
struct InsnInRetire<C: PhantomConstGet<CpuConfig>> {
insn: WipDecodedInsn,
config: C,
}
impl<C: Type + PhantomConstGet<CpuConfig>> InsnInRetire<C> {
#[hdl]
fn from_insn_in_rename_dispatch_execute(
v: SimValue<InsnInRenameDispatchExecute<C>>,
) -> SimValue<Self> {
#[hdl(sim)]
let InsnInRenameDispatchExecute::<_> { insn, config } = v;
#[hdl(sim)]
Self { insn, config }
}
}
#[hdl]
enum BranchPredictionState {
StronglyNotTaken,
@ -221,6 +723,12 @@ impl<T: SimOnlyValueTrait> SimValueDefault for SimOnly<T> {
}
}
impl<T: SimValueDefault, N: KnownSize> SimValueDefault for ArrayVec<T, N> {
fn sim_value_default(self) -> SimValue<Self> {
self.new_sim(self.element().sim_value_default())
}
}
impl<T: Type> SimValueDefault for HdlOption<T> {
fn sim_value_default(self) -> SimValue<Self> {
self.HdlNone().to_sim_value_with_type(self)
@ -411,27 +919,51 @@ impl BTBEntryAddrKind {
}
#[hdl]
struct BTBEntry {
/// address of first instruction to run in this fetch block
start_pc: UInt<64>,
struct BTBEntryWithoutStartPc {
target_pc: UInt<64>,
/// when branch is not taken, the next pc to fetch from is `start_pc + fallthrough_offset`.
/// needed because there may be more than one branch in a fetch block
fallthrough_offset: UInt<8>,
/// the pc to use for branch prediction is `start_pc + branch_offset`
branch_offset: UInt<8>,
/// when a call is made, the return address is `start_pc + after_call_offset`
after_call_offset: UInt<8>,
insn_kind: BTBEntryInsnKind,
addr_kind: BTBEntryAddrKind,
}
#[hdl]
struct BTBEntry {
/// address of first instruction to run in this fetch block
start_pc: UInt<64>,
rest: BTBEntryWithoutStartPc,
}
impl BTBEntry {
fn taken_pc(this: &SimValue<Self>) -> u64 {
this.target_pc.as_int()
this.rest.target_pc.as_int()
}
fn not_taken_fetch_pc(this: &SimValue<Self>) -> u64 {
fn not_taken_start_pc(this: &SimValue<Self>) -> u64 {
Self::fallthrough_pc(this)
}
/// when branch is not taken, this returns the next pc to fetch from.
/// needed because there may be more than one branch in a fetch block
fn fallthrough_pc(this: &SimValue<Self>) -> u64 {
this.start_pc
.as_int()
.wrapping_add(this.fallthrough_offset.as_int().into())
.wrapping_add(this.rest.fallthrough_offset.as_int().into())
}
/// the pc to use for branch prediction
fn branch_pc(this: &SimValue<Self>) -> u64 {
this.start_pc
.as_int()
.wrapping_add(this.rest.branch_offset.as_int().into())
}
/// when a call is made, this gives the return address
fn after_call_pc(this: &SimValue<Self>) -> u64 {
this.start_pc
.as_int()
.wrapping_add(this.rest.after_call_offset.as_int().into())
}
}
@ -489,11 +1021,15 @@ impl SimValueDefault for BranchTargetBuffer {
#[hdl(sim)]
BTBEntry {
start_pc: !0u64,
target_pc: !0u64,
fallthrough_offset: !0u8,
after_call_offset: !0u8,
insn_kind: BTBEntryInsnKind.Call(),
addr_kind: BTBEntryAddrKind.CondNotTaken(),
rest: #[hdl(sim)]
BTBEntryWithoutStartPc {
target_pc: !0u64,
fallthrough_offset: !0u8,
branch_offset: !0u8,
after_call_offset: !0u8,
insn_kind: BTBEntryInsnKind.Call(),
addr_kind: BTBEntryAddrKind.CondNotTaken(),
},
},
); Self::SIZE],
next_index_to_replace_lfsr: LFSR31.sim_value_default(),
@ -1007,13 +1543,13 @@ impl<C: Type + PhantomConstGet<CpuConfig>> NextPcState<C> {
// for now we just truncate the fetch block right before the second ctrl transfer insn.
break;
}
btb_entry_fields = Some((insn_kind, addr_kind, target_pc));
fallthrough_offset += size_in_bytes.cast_to_static::<UInt<8>>().as_int();
#[hdl(sim)]
match insn_kind {
match &insn_kind {
BTBEntryInsnKind::Call => after_call_offset = fallthrough_offset,
BTBEntryInsnKind::Branch | BTBEntryInsnKind::Ret | BTBEntryInsnKind::Unknown => {}
}
btb_entry_fields = Some((insn_kind, addr_kind, target_pc));
fallthrough_offset += size_in_bytes.cast_to_static::<UInt<8>>().as_int();
}
let new_next_pc = if let Some((insn_kind, addr_kind, mut target_pc)) = btb_entry_fields {
// add/update BTBEntry if it doesn't match
@ -1046,15 +1582,22 @@ impl<C: Type + PhantomConstGet<CpuConfig>> NextPcState<C> {
BranchTargetBuffer::next_index_to_replace(&mut this.branch_target_buffer)
});
let new_next_pc = #[hdl(sim)]
match insn_kind {
BTBEntryInsnKind::Branch => {}
match &insn_kind {
BTBEntryInsnKind::Branch => {
todo!()
}
BTBEntryInsnKind::Call => {
CallStack::push(&mut this.speculative_call_stack, todo!());
CallStack::push(
&mut this.speculative_call_stack,
start_pc + u64::from(after_call_offset),
);
todo!()
}
BTBEntryInsnKind::Ret => {
target_pc = CallStack::pop(&mut this.speculative_call_stack).or(target_pc);
target_pc.unwrap_or(0u64)
}
BTBEntryInsnKind::Unknown => unreachable!(),
};
let new_entry = #[hdl(sim)]
BTBEntry {

View file

@ -0,0 +1,25 @@
stateDiagram-v2
direction LR
state "Next PC" as next_pc
[*] --> next_pc
state "Fetch/Decode" as fetch_decode
next_pc --> fetch_decode
state "Branch Predictor" as br_pred
next_pc --> br_pred
br_pred --> next_pc: cancel following
state "Post-decode" as post_decode
fetch_decode --> post_decode
br_pred --> post_decode
post_decode --> next_pc: cancel following
state "Rename\nDispatch\nExecute" as execute
post_decode --> execute
state "Retire" as retire
execute --> retire
retire --> [*]
retire --> next_pc: cancel following

View file

@ -34,6 +34,18 @@ impl<T: Type, N: Size> ArrayVec<T, N> {
len: 0u8.cast_to(self.len),
}
}
#[hdl]
pub fn new_full_sim(
self,
elements: impl ToSimValueWithType<ArrayType<T, N>>,
) -> SimValue<Self> {
let elements = elements.to_sim_value_with_type(self.elements);
#[hdl(sim)]
Self {
elements,
len: self.elements.len().to_sim_value_with_type(self.len),
}
}
pub fn element(self) -> T {
self.elements.element()
}