Support class srandom and class random stability.

This commit is contained in:
Wilson Snyder 2023-04-01 10:50:27 -04:00
parent 28eadded87
commit 69121633cf
21 changed files with 361 additions and 22 deletions

View File

@ -17,6 +17,7 @@ Verilator 5.009 devel
* Add --public-params flag (#3990). [Andrew Nolte]
* Add STATICVAR warning and convert to automatic (#4018) (#4027) (#4030). [Ryszard Rozak, Antmicro Ltd]
* Support class extends of package::class.
* Support class srandom and class random stability.
* Support method calls without parenthesis (#4034). [Ryszard Rozak, Antmicro Ltd]
* Support complicated IEEE 'for' assignments.
* Support $fopen as an expression.

View File

@ -279,6 +279,52 @@ void VL_PRINTF_MT(const char* formatp, ...) VL_MT_SAFE {
//===========================================================================
// Random -- Mostly called at init time, so not inline.
VlRNG::VlRNG() VL_MT_SAFE {
// Starting point for this new class comes from the global RNG
VlRNG& fromr = vl_thread_rng();
m_state = fromr.m_state;
// Advance the *source* so it can later generate a new number
// Xoroshiro128+ algorithm
fromr.m_state[1] ^= fromr.m_state[0];
fromr.m_state[0] = (((fromr.m_state[0] << 55) | (fromr.m_state[0] >> 9)) ^ fromr.m_state[1]
^ (fromr.m_state[1] << 14));
fromr.m_state[1] = (fromr.m_state[1] << 36) | (fromr.m_state[1] >> 28);
}
uint64_t VlRNG::rand64() VL_MT_UNSAFE {
// Xoroshiro128+ algorithm
const uint64_t result = m_state[0] + m_state[1];
m_state[1] ^= m_state[0];
m_state[0] = (((m_state[0] << 55) | (m_state[0] >> 9)) ^ m_state[1] ^ (m_state[1] << 14));
m_state[1] = (m_state[1] << 36) | (m_state[1] >> 28);
return result;
}
uint64_t VlRNG::vl_thread_rng_rand64() VL_MT_SAFE {
VlRNG& fromr = vl_thread_rng();
const uint64_t result = fromr.m_state[0] + fromr.m_state[1];
fromr.m_state[1] ^= fromr.m_state[0];
fromr.m_state[0] = (((fromr.m_state[0] << 55) | (fromr.m_state[0] >> 9)) ^ fromr.m_state[1]
^ (fromr.m_state[1] << 14));
fromr.m_state[1] = (fromr.m_state[1] << 36) | (fromr.m_state[1] >> 28);
return result;
}
void VlRNG::srandom(uint64_t n) VL_MT_UNSAFE {
m_state[0] = n;
m_state[1] = m_state[0];
// Fix state as algorithm is slow to randomize if many zeros
// This causes a loss of ~ 1 bit of seed entropy, no big deal
if (VL_COUNTONES_I(m_state[0]) < 10) m_state[0] = ~m_state[0];
if (VL_COUNTONES_I(m_state[1]) < 10) m_state[1] = ~m_state[1];
}
// Unused: void VlRNG::set_randstate(const std::string& state) VL_MT_UNSAFE {
// Unused: if (VL_LIKELY(state.length() == sizeof(m_state))) {
// Unused: memcpy(m_state, state.data(), sizeof(m_state));
// Unused: }
// Unused: }
// Unused: std::string VlRNG::get_randstate() const VL_MT_UNSAFE {
// Unused: std::string out{reinterpret_cast<const char *>(&m_state), sizeof(m_state)};
// Unused: return out;
// Unused: }
static uint32_t vl_sys_rand32() VL_MT_SAFE {
// Return random 32-bits using system library.
// Used only to construct seed for Verilator's PRNG.
@ -292,27 +338,23 @@ static uint32_t vl_sys_rand32() VL_MT_SAFE {
#endif
}
uint64_t vl_rand64() VL_MT_SAFE {
static thread_local uint64_t t_state[2];
VlRNG& VlRNG::vl_thread_rng() VL_MT_SAFE {
static thread_local VlRNG t_rng{0};
static thread_local uint32_t t_seedEpoch = 0;
// For speed, we use a thread-local epoch number to know when to reseed
// A thread always belongs to a single context, so this works out ok
if (VL_UNLIKELY(t_seedEpoch != VerilatedContextImp::randSeedEpoch())) {
// Set epoch before state, to avoid race case with new seeding
t_seedEpoch = VerilatedContextImp::randSeedEpoch();
t_state[0] = Verilated::threadContextp()->impp()->randSeedDefault64();
t_state[1] = t_state[0];
// Same as srandom() but here as needs to be VL_MT_SAFE
t_rng.m_state[0] = Verilated::threadContextp()->impp()->randSeedDefault64();
t_rng.m_state[1] = t_rng.m_state[0];
// Fix state as algorithm is slow to randomize if many zeros
// This causes a loss of ~ 1 bit of seed entropy, no big deal
if (VL_COUNTONES_I(t_state[0]) < 10) t_state[0] = ~t_state[0];
if (VL_COUNTONES_I(t_state[1]) < 10) t_state[1] = ~t_state[1];
if (VL_COUNTONES_I(t_rng.m_state[0]) < 10) t_rng.m_state[0] = ~t_rng.m_state[0];
if (VL_COUNTONES_I(t_rng.m_state[1]) < 10) t_rng.m_state[1] = ~t_rng.m_state[1];
}
// Xoroshiro128+ algorithm
const uint64_t result = t_state[0] + t_state[1];
t_state[1] ^= t_state[0];
t_state[0] = (((t_state[0] << 55) | (t_state[0] >> 9)) ^ t_state[1] ^ (t_state[1] << 14));
t_state[1] = (t_state[1] << 36) | (t_state[1] >> 28);
return result;
return t_rng;
}
WDataOutP VL_RANDOM_W(int obits, WDataOutP outwp) VL_MT_SAFE {
@ -321,6 +363,12 @@ WDataOutP VL_RANDOM_W(int obits, WDataOutP outwp) VL_MT_SAFE {
return outwp;
}
WDataOutP VL_RANDOM_RNG_W(VlRNG& rngr, int obits, WDataOutP outwp) VL_MT_UNSAFE {
for (int i = 0; i < VL_WORDS_I(obits); ++i) outwp[i] = rngr.rand64();
// Last word is unclean
return outwp;
}
IData VL_RANDOM_SEEDED_II(IData& seedr) VL_MT_SAFE {
// $random - seed is a new seed to apply, then we return new seed
Verilated::threadContextp()->randSeed(static_cast<int>(seedr));

View File

@ -136,14 +136,34 @@ public:
};
inline std::string VL_TO_STRING(const VlEvent& e) {
return std::string("triggered=") + (e.isTriggered() ? "true" : "false");
return std::string{"triggered="} + (e.isTriggered() ? "true" : "false");
}
//===================================================================
// Shuffle RNG
// Random
extern uint64_t vl_rand64() VL_MT_SAFE;
// Random Number Generator with internal state
class VlRNG final {
std::array<uint64_t, 2> m_state;
public:
// The default constructor simply sets state, to avoid vl_rand64()
// having to check for construction at each call
// Alternative: seed with zero and check on rand64() call
VlRNG() VL_MT_SAFE;
VlRNG(uint64_t seed0) VL_MT_SAFE : m_state{0x12341234UL, seed0} {}
void srandom(uint64_t n) VL_MT_UNSAFE;
// Unused: std::string get_randstate() const VL_MT_UNSAFE;
// Unused: void set_randstate(const std::string& state) VL_MT_UNSAFE;
uint64_t rand64() VL_MT_UNSAFE;
// Threadsafe, but requires use on vl_thread_rng
static uint64_t vl_thread_rng_rand64() VL_MT_SAFE;
static VlRNG& vl_thread_rng() VL_MT_SAFE;
};
inline uint64_t vl_rand64() VL_MT_SAFE { return VlRNG::vl_thread_rng_rand64(); }
// RNG for shuffle()
class VlURNG final {
public:
using result_type = size_t;
@ -152,6 +172,11 @@ public:
size_t operator()() { return VL_MASK_I(31) & vl_rand64(); }
};
// These require the class object to have the thread safety lock
inline IData VL_RANDOM_RNG_I(VlRNG& rngr) VL_MT_UNSAFE { return rngr.rand64(); }
inline QData VL_RANDOM_RNG_Q(VlRNG& rngr) VL_MT_UNSAFE { return rngr.rand64(); }
extern WDataOutP VL_RANDOM_RNG_W(VlRNG& rngr, int obits, WDataOutP outwp) VL_MT_UNSAFE;
//===================================================================
// Readmem/Writemem operation classes

View File

@ -1620,6 +1620,26 @@ public:
bool reset() const { return m_reset; }
bool urandom() const { return m_urandom; }
};
class AstRandRNG final : public AstNodeExpr {
// Random used in a class using VlRNG
// Return a random number, based upon width()
public:
AstRandRNG(FileLine* fl, AstNodeDType* dtp)
: ASTGEN_SUPER_RandRNG(fl) {
dtypep(dtp);
}
ASTGEN_MEMBERS_AstRandRNG;
string emitVerilog() override { return "%f$rngrandom()"; }
string emitC() override {
return isWide() ? "VL_RANDOM_RNG_%nq(__Vm_rng, %nw, %P)" //
: "VL_RANDOM_RNG_%nq(__Vm_rng)";
}
bool cleanOut() const override { return false; }
bool isGateOptimizable() const override { return false; }
bool isPredictOptimizable() const override { return false; }
int instrCount() const override { return INSTR_COUNT_PLI; }
bool same(const AstNode* /*samep*/) const override { return true; }
};
class AstRose final : public AstNodeExpr {
// Verilog $rose
// @astgen op1 := exprp : AstNodeExpr

View File

@ -2116,6 +2116,7 @@ class AstClass final : public AstNodeModule {
AstClassPackage* m_classOrPackagep = nullptr; // Class package this is under
bool m_extended = false; // Is extension or extended by other classes
bool m_interfaceClass = false; // Interface class
bool m_needRNG = false; // Need RNG, uses srandom/randomize
bool m_virtual = false; // Virtual class
void insertCache(AstNode* nodep);
@ -2148,9 +2149,13 @@ public:
void isInterfaceClass(bool flag) { m_interfaceClass = flag; }
bool isVirtual() const { return m_virtual; }
void isVirtual(bool flag) { m_virtual = flag; }
bool needRNG() const { return m_needRNG; }
void needRNG(bool flag) { m_needRNG = flag; }
// Return true if this class is an extension of base class (SLOW)
// Accepts nullptrs
static bool isClassExtendedFrom(const AstClass* refClassp, const AstClass* baseClassp);
// Return the lowest class extended from, or this class
AstClass* baseMostClassp();
};
class AstClassPackage final : public AstNodeModule {
// The static information portion of a class (treated similarly to a package)

View File

@ -1455,6 +1455,13 @@ void AstClass::repairCache() {
}
}
}
AstClass* AstClass::baseMostClassp() {
AstClass* basep = this;
while (basep->extendsp() && basep->extendsp()->classp()) {
basep = basep->extendsp()->classp();
}
return basep;
}
bool AstClass::isClassExtendedFrom(const AstClass* refClassp, const AstClass* baseClassp) {
// TAIL RECURSIVE
if (!refClassp || !baseClassp) return false;

View File

@ -905,6 +905,9 @@ public:
void visit(AstRand* nodep) override {
emitOpName(nodep, nodep->emitC(), nodep->seedp(), nullptr, nullptr);
}
void visit(AstRandRNG* nodep) override {
emitOpName(nodep, nodep->emitC(), nullptr, nullptr, nullptr);
}
void visit(AstTime* nodep) override {
puts("VL_TIME_UNITED_Q(");
if (nodep->timeunit().isNone()) nodep->v3fatalSrc("$time has no units");

View File

@ -117,7 +117,12 @@ class EmitCHeader final : public EmitCConstInit {
emitCurrentList();
}
void emitInternalVarDecls(const AstNodeModule* modp) {
if (!VN_IS(modp, Class)) {
if (const AstClass* const classp = VN_CAST(modp, Class)) {
if (classp->needRNG()) {
putsDecoration("\n// INTERNAL VARIABLES\n");
puts("VlRNG __Vm_rng;\n");
}
} else { // not class
putsDecoration("\n// INTERNAL VARIABLES\n");
puts(symClassName() + "* const vlSymsp;\n");
}

View File

@ -3102,6 +3102,8 @@ private:
}
} else if (VN_IS(nodep, New) && m_statep->forPrearray()) {
// Resolved in V3Width
} else if (nodep->name() == "randomize" || nodep->name() == "srandom") {
// Resolved in V3Width
} else if (nodep->dotted() == "") {
if (nodep->pli()) {
if (v3Global.opt.bboxSys()) {

View File

@ -115,7 +115,7 @@ private:
if (m_underGenerate) nodep->underGenerate(true);
// Remember the existing symbol table scope
if (m_classp) {
if (nodep->name() == "randomize") {
if (nodep->name() == "randomize" || nodep->name() == "srandom") {
nodep->v3error(nodep->prettyNameQ()
<< " is a predefined class method; redefinition not allowed"
" (IEEE 1800-2017 18.6.3)");

View File

@ -287,6 +287,10 @@ private:
iterateChildren(nodep);
checkNode(nodep);
}
void visit(AstRandRNG* nodep) override {
iterateChildren(nodep);
checkNode(nodep);
}
void visit(AstUCFunc* nodep) override {
iterateChildren(nodep);
checkNode(nodep);

View File

@ -188,15 +188,15 @@ private:
AstVarRef* const tabRefp
= new AstVarRef{fl, enumValueTabp(enumDtp), VAccess::READ};
tabRefp->classOrPackagep(v3Global.rootp()->dollarUnitPkgAddp());
AstRand* const randp = new AstRand{fl, nullptr, false};
AstRandRNG* const randp
= new AstRandRNG{fl, varrefp->findBasicDType(VBasicDTypeKwd::UINT32)};
AstNodeExpr* const moddivp = new AstModDiv{
fl, randp, new AstConst{fl, static_cast<uint32_t>(enumDtp->itemCount())}};
randp->dtypep(varrefp->findBasicDType(VBasicDTypeKwd::UINT32));
moddivp->dtypep(enumDtp);
valp = new AstArraySel{fl, tabRefp, moddivp};
} else {
valp = new AstRand{fl, nullptr, false};
valp->dtypep(memberp ? memberp->dtypep() : varrefp->varp()->dtypep());
valp = new AstRandRNG{fl,
(memberp ? memberp->dtypep() : varrefp->varp()->dtypep())};
}
return new AstAssign{fl,
new AstSel{fl, varrefp, offset + (memberp ? memberp->lsb() : 0),
@ -374,6 +374,7 @@ void V3Randomize::randomizeNetlist(AstNetlist* nodep) {
AstFunc* V3Randomize::newRandomizeFunc(AstClass* nodep) {
AstFunc* funcp = VN_AS(nodep->findMember("randomize"), Func);
if (!funcp) {
v3Global.useRandomizeMethods(true);
AstNodeDType* const dtypep
= nodep->findBitDType(32, 32, VSigning::SIGNED); // IEEE says int return of 0/1
AstVar* const fvarp = new AstVar{nodep->fileline(), VVarType::MEMBER, "randomize", dtypep};
@ -387,6 +388,31 @@ AstFunc* V3Randomize::newRandomizeFunc(AstClass* nodep) {
funcp->isVirtual(nodep->isExtended());
nodep->addMembersp(funcp);
nodep->repairCache();
AstClass* const basep = nodep->baseMostClassp();
basep->needRNG(true);
}
return funcp;
}
AstFunc* V3Randomize::newSRandomFunc(AstClass* nodep) {
AstClass* const basep = nodep->baseMostClassp();
AstFunc* funcp = VN_AS(basep->findMember("srandom"), Func);
if (!funcp) {
v3Global.useRandomizeMethods(true);
AstNodeDType* const dtypep
= basep->findBitDType(32, 32, VSigning::SIGNED); // IEEE says argument 0/1
AstVar* const ivarp = new AstVar{basep->fileline(), VVarType::MEMBER, "seed", dtypep};
ivarp->lifetime(VLifetime::AUTOMATIC);
ivarp->funcLocal(true);
ivarp->direction(VDirection::INPUT);
funcp = new AstFunc{basep->fileline(), "srandom", ivarp, nullptr};
funcp->dtypep(basep->findVoidDType());
funcp->classMethod(true);
funcp->isVirtual(false);
basep->addMembersp(funcp);
basep->repairCache();
funcp->addStmtsp(new AstCStmt{basep->fileline(), "__Vm_rng.srandom(seed);\n"});
basep->needRNG(true);
}
return funcp;
}

View File

@ -29,6 +29,7 @@ public:
static void randomizeNetlist(AstNetlist* nodep);
static AstFunc* newRandomizeFunc(AstClass* nodep);
static AstFunc* newSRandomFunc(AstClass* nodep);
};
#endif // Guard

View File

@ -224,6 +224,7 @@ private:
// STATE
WidthVP* m_vup = nullptr; // Current node state
AstClass* m_classp = nullptr; // Current class
const AstCell* m_cellp = nullptr; // Current cell for arrayed instantiations
const AstEnumItem* m_enumItemp = nullptr; // Current enum item
const AstNodeFTask* m_ftaskp = nullptr; // Current function/task
@ -2596,6 +2597,8 @@ private:
if (nodep->didWidthAndSet()) return;
// Must do extends first, as we may in functions under this class
// start following a tree of extends that takes us to other classes
VL_RESTORER(m_classp);
m_classp = nodep;
userIterateAndNext(nodep->extendsp(), nullptr);
userIterateChildren(nodep, nullptr); // First size all members
nodep->repairCache();
@ -2611,6 +2614,7 @@ private:
// userIterateChildren(nodep->classp(), nullptr);
}
void visit(AstNodeModule* nodep) override {
// Visitor does not include AstClass - specialized visitor above
VL_RESTORER(m_modp);
m_modp = nodep;
userIterateChildren(nodep, nullptr);
@ -3460,8 +3464,9 @@ private:
// No need to width-resolve the class, as it was done when we did the child
AstClass* const first_classp = adtypep->classp();
if (nodep->name() == "randomize") {
v3Global.useRandomizeMethods(true);
V3Randomize::newRandomizeFunc(first_classp);
} else if (nodep->name() == "srandom") {
V3Randomize::newSRandomFunc(first_classp);
}
UASSERT_OBJ(first_classp, nodep, "Unlinked");
for (AstClass* classp = first_classp; classp;) {
@ -5504,6 +5509,21 @@ private:
// For arguments, is assignment-like context; see IEEE rules in AstNodeAssign
// Function hasn't been widthed, so make it so.
UINFO(5, " FTASKREF " << nodep << endl);
if (nodep->name() == "randomize" || nodep->name() == "srandom") {
// TODO perhaps this should move to V3LinkDot
if (!m_classp) {
nodep->v3error("Calling implicit class method " << nodep->prettyNameQ()
<< " without being under class");
nodep->replaceWith(new AstConst{nodep->fileline(), 0});
VL_DO_DANGLING(pushDeletep(nodep), nodep);
return;
}
if (nodep->name() == "randomize") {
nodep->taskp(V3Randomize::newRandomizeFunc(m_classp));
} else {
nodep->taskp(V3Randomize::newSRandomFunc(m_classp));
}
}
UASSERT_OBJ(nodep->taskp(), nodep, "Unlinked");
if (nodep->didWidth()) return;
userIterate(nodep->taskp(), nullptr);

View File

@ -4,4 +4,7 @@
%Error: t/t_randomize_method_bad.v:14:18: 'randomize' is a predefined class method; redefinition not allowed (IEEE 1800-2017 18.6.3)
14 | function void randomize(int x);
| ^~~~~~~~~
%Error: t/t_randomize_method_bad.v:16:18: 'srandom' is a predefined class method; redefinition not allowed (IEEE 1800-2017 18.6.3)
16 | function void srandom(int seed);
| ^~~~~~~
%Error: Exiting due to

View File

@ -13,6 +13,8 @@ endclass
class Cls2;
function void randomize(int x);
endfunction
function void srandom(int seed);
endfunction
endclass
module t (/*AUTOARG*/);

View File

@ -0,0 +1,9 @@
%Error: t/t_randomize_method_nclass_bad.v:9:7: Calling implicit class method 'randomize' without being under class
: ... In instance t
9 | randomize(1);
| ^~~~~~~~~
%Error: t/t_randomize_method_nclass_bad.v:10:7: Calling implicit class method 'srandom' without being under class
: ... In instance t
10 | srandom(1);
| ^~~~~~~
%Error: Exiting due to

View File

@ -0,0 +1,19 @@
#!/usr/bin/env perl
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# Copyright 2020 by Wilson Snyder. This program is free software; you
# can redistribute it and/or modify it under the terms of either the GNU
# Lesser General Public License Version 3 or the Perl Artistic License
# Version 2.0.
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
scenarios(linter => 1);
lint(
fails => $Self->{vlt_all},
expect_filename => $Self->{golden_filename},
);
ok(1);
1;

View File

@ -0,0 +1,12 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2023 by Wilson Snyder.
// SPDX-License-Identifier: CC0-1.0
module t (/*AUTOARG*/);
initial begin
randomize(1);
srandom(1);
end
endmodule

View File

@ -0,0 +1,21 @@
#!/usr/bin/env perl
if (!$::Driver) { use FindBin; exec("$FindBin::Bin/bootstrap.pl", @ARGV, $0); die; }
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# Copyright 2020 by Wilson Snyder. This program is free software; you
# can redistribute it and/or modify it under the terms of either the GNU
# Lesser General Public License Version 3 or the Perl Artistic License
# Version 2.0.
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
scenarios(simulator => 1);
compile(
);
execute(
check_finished => 1,
);
ok(1);
1;

View File

@ -0,0 +1,106 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2023 by Wilson Snyder.
// SPDX-License-Identifier: CC0-1.0
`define stop $stop
`define checkeq(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0)
`define checkne(gotv,expv) do if ((gotv) === (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0)
class Cls;
bit [63:0] m_sum;
rand int m_r;
function void hash_init();
m_sum = 64'h5aef0c8d_d70a4497;
endfunction
function void hash(int res);
$display(" res %x", res);
m_sum = {32'h0, res} ^ {m_sum[62:0], m_sum[63] ^ m_sum[2] ^ m_sum[0]};
endfunction
function bit [63:0] test1();
Cls o;
// Affected by srandom
$display(" init for randomize");
hash_init;
// TODO: Support this.randomize()
o = this;
void'(o.randomize());
hash(m_r);
void'(o.randomize());
hash(m_r);
return m_sum;
endfunction
function bit [63:0] test2(int seed);
$display(" init for seeded randomize");
hash_init;
this.srandom(seed);
void'(this.randomize());
hash(m_r);
return m_sum;
endfunction
function bit [63:0] test3(int seed);
$display(" init for seeded randomize");
hash_init;
srandom(seed);
void'(randomize());
hash(m_r);
return m_sum;
endfunction
endclass
module t(/*AUTOARG*/);
Cls ca;
Cls cb;
bit [63:0] sa;
bit [63:0] sb;
initial begin
// Each class gets different seed from same thread,
// so the randomization should be different
$display("New");
ca = new;
cb = new;
sa = ca.test1();
sb = cb.test1();
`checkne(sa, sb); // Could false-fail 2^-32
// Seed the classes to be synced
$display("Seed");
ca.srandom(123);
cb.srandom(123);
sa = ca.test1();
sb = cb.test1();
`checkeq(sa, sb);
// Check using this
$display("this.srandom");
sa = ca.test2(1);
sb = cb.test2(2);
`checkne(sa, sb);
sa = ca.test2(3);
sb = cb.test2(3);
`checkeq(sa, sb);
// Check using direct call
$display("srandom");
sa = ca.test3(1);
sb = cb.test3(2);
`checkne(sa, sb);
sa = ca.test3(3);
sb = cb.test3(3);
`checkeq(sa, sb);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule