Skip to content
VLSI Mentor

C Part II · Module 21 · Arrays and Open Arrays in DPI-C

Open Array Elements and Bounds

Chapter 21.4's mistake was loud: cast the handle and an address turns up where an element should be. This one is quiet. Index from zero while the counter is under the size — the most ordinary loop in C — and against an array declared over the range 3 to 10 it asks for three indices that do not exist, never touches three that do, and files the five it did read under the wrong numbers. Through the API, one of those three failures is reported. Through a bare pointer, none is, and five genuine values come back with nothing anywhere to complain about.

Expert23 min readCPart IIVerificationDPI-C

Part II · Module 21 · Chapter 21.5 · Open Array Elements and Bounds

1. What to Ask, and in What Order

Chapter 21.4 established that an open array arrives as an opaque handle with a set of functions on it. This chapter uses them, and the order is the content.

From the LRM, the queries answer four things, and a C function that is going to touch elements needs three of them before it touches any:

  • the low index of the dimension
  • the high index
  • the number of elements
  • the bytes per element

Measured on an array declared over the range 3 to 10:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  low  = 3
  high = 10
  size = 8 elements
  bytes per element = 4

Note that size and low are independent facts. Eight elements, and the first one is number 3. A C array cannot be like that, which is why this is the chapter where C habits stop transferring.

2. The Loop That Is Correct

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for (int i = oaLow(h); i <= oaHigh(h); i++) {
    uint32_t v = *(uint32_t *)oaElemPtr(h, i);
    …
}

Two details, both load-bearing.

The bound is <=, not <. high is the index of the last element, not one past it — the opposite of every C convention, including the size the same API will hand you.

The index comes from the array, not from a counter. Nothing in the loop assumes anything about where the indices begin.

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  for (i = low; i <= high; i++): 0x100 0x101 0x102 0x103 0x104 0x105 0x106 0x107

All eight, once each. And the round trip:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  8 elements read back: exact

3. The Loop Written by Reflex

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for (int i = 0; i < size; i++) { … oaElemPtr(h, i) … }

It is the right loop for a C array and it is correct for a great many SystemVerilog arrays too, which is exactly the problem. Measured against the array declared 3 to 10:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  for (i = 0; i < size; i++):   -- -- -- 0x100 0x101 0x102 0x103 0x104
  indices 0,1,2 are OUTSIDE the declared range: 3 rejected, 5 returned
  and the 5 that returned are elements 3..7, not 0..4 -- SHIFTED by 3

Three separate things went wrong in one line.

It asked for three indices that do not exist. 0, 1 and 2 are outside the declared range. Asked through the API, they can be rejected.

It never read three that do. Elements 8, 9 and 10 were not touched. The loop stopped after eight iterations and it had started three too early.

It mislabelled the five it did read. Element 3 was recorded as element 0, element 4 as element 1, and so on — shifted by 3.

An open array declared over the range 3 to 10, and the two loops a C function might write over it. Measured, the handle reports a low index of 3, a high index of 10 and a size of 8 elements. The reflex loop runs the index from 0 while it is less than the size, so it asks for indices 0 through 7. Indices 0, 1 and 2 are outside the declared range and are rejected, leaving five elements returned; those five are elements 3 through 7, so elements 8, 9 and 10 are never read at all, and the five that were read are being labelled 0 through 4. The loop the handle asks for runs the index from low to high inclusive, reads all eight elements exactly once, and assumes nothing about where the indices begin: measured, the round trip over the declared range is exact.The array as declaredan unpacked range of 3 to 10 — measured: low 3, high 10, size 830x10040x10150x10260x10370x10480x10590x106100x107eight elementsfirst index 3, not 0The loop written by reflexfor (i = 0; i < size; i++) — measured: 3 rejected, 5 returned0rejected1rejected2rejected30x10040x10150x10260x10370x104elements 8, 9 and10 never readand five aremislabelledThe loop the handle asks forfor (i = low; i <= high; i++) — measured: all 8, round trip exact30x10040x10150x10260x10370x10480x10590x106100x107every element,oncenothing assumed12
Figure 1 — one array, two loops, drawn element by element. The top strip is the array as declared: eight elements whose first index is 3, which no C array can be. The middle strip is the reflex loop, and the three failures are all visible in it at once — indices 0, 1 and 2 are rejected because they do not exist, elements 8, 9 and 10 at the right-hand end are never reached, and the five that did come back are being recorded under the wrong numbers, each shifted down by three. The bottom strip is the loop the handle asks for: the index runs from low to high inclusive, every element is read exactly once, and nothing in it assumes where the numbering starts. The measured round trip over that range is exact.

4. The Finding: Only One of the Three Is Detectable

Count what a tool could possibly have told you about §3.

The out-of-range indices can be reported — but only because they went through the API, which knows the bounds and can refuse. That is the entire value of asking rather than computing.

The three unread elements cannot be detected by anything. Not reading something is not an error. There is no sanitizer for insufficient curiosity.

The mislabelling cannot be detected either. Element 3's value stored under the name 0 is a correct read of a real element filed in the wrong place, and no tool has an opinion about filing.

Now remove the API. Measured, the same mistake with a raw pointer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  raw[0] = 0x100, but element 0 does not exist; element 3 is 0x100
  the VALUES coincide here, because element 3 is stored first
  so the reflex loop reads real data and is wrong about WHICH data

With a bare pointer the first failure disappears too — not because it stopped happening, but because nothing is left to notice. raw[0] returns 0x100, which is a real element's real value. The loop runs to completion, reads five genuine values, and is wrong about every one of their identities.

Every byte touched belongs to the buffer, so this is Chapter 21.1 §6's boundary once more: no memory error, nothing to report, a plausible answer.

And it is the sharp contrast with Chapter 21.4. There, casting the handle produced an address where a value should be — strange, large, noticeable. Here the values are ordinary and correct and attached to the wrong indices. The loud mistake is the one people find in an afternoon; this is the other kind.

5. Writing It So the Question Cannot Arise

Four habits, each removing one way to get §3 wrong.

Take the bounds from the array, every time. Two calls at the top of the function, and no index literal anywhere below them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
const int lo = oaLow(h), hi = oaHigh(h);
for (int i = lo; i <= hi; i++) { … }

Use <= with high, and < with size — never mix them. Pick the bounds form and use it throughout; the two idioms are both correct and produce different loops, and the hybrid for (i = low; i < size; i++) is wrong in a way that looks careful.

Get the element size from the array too. Chapter 21.2 measured what happens when a stride is assumed: the values decode to plausible wrong numbers. The handle carries it, so there is no reason to write a 4.

Convert to your own representation once, at the top. If the model's natural shape is a zero-based C array — and it usually is — copy into one immediately, and do the index translation in exactly one place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for (int i = lo; i <= hi; i++) local[i - lo] = *(uint32_t *)oaElemPtr(h, i);

The i - lo is the entire translation. Written once it is reviewable; spread through a function it is where §3 comes from.

6. The Misconception: low Is Always Zero in Practice

"Ranges that do not start at zero are a curiosity. Every array I have seen is [0:N-1], so for (i = 0; i < size; i++) is a safe default."

The first half is measured true — [0:7] gives low = 0 — and the conclusion still does not hold, for three reasons.

"Safe default" is the wrong category. A default is safe when being wrong about it is detectable. §4 measured that two of the three failures are invisible to every tool, and that with a bare pointer the third one is too. An assumption whose violation cannot be observed is not a default; it is a dependency.

You are not choosing the declaration. The array belongs to the SystemVerilog side, and a range is an ordinary thing for a verification engineer to write — a register map indexed by address, a channel array numbered from one, a window declared [START:END]. Nothing warns anybody that a C model elsewhere depends on the lower bound.

The alternative costs two calls. Not a design change, not a rewrite, not a performance trade — oaLow and oaHigh into two local variables, after which the loop body is unchanged.

The corrected position:

The reflex loop is correct for the common declaration, which is why it survives review and why its failure is not a thing you will be warned about. Taking the bounds from the array costs two calls and removes the dependency entirely — and the C you write below them is identical either way.

7. What You Should Be Able to Say Now

  1. Ask before touching: low, high, size and bytes per element. Measured on a 3 to 10 array: 3, 10, 8, 4.
  2. size and low are independent — eight elements whose first index is 3, which no C array can be.
  3. The correct loop is for (i = low; i <= high; i++) — high is the last index, not one past it. Measured: all eight values, round trip exact.
  4. The reflex loop does three wrong things at once — measured: 3 indices rejected, elements 8, 9, 10 never read, and the five that were read shifted by 3.
  5. Only the first is detectable, and only through the API, which knows the bounds and can refuse.
  6. Not reading an element is not an error, and mislabelling one is not either — no tool has an opinion about filing.
  7. With a bare pointer, even the first failure disappears — measured, raw[0] returns 0x100, a real element's real value, and nothing complains.
  8. This is the quiet counterpart to Chapter 21.4's loud one: there, an address where a value should be; here, correct values under wrong names.
  9. Four habits: take the bounds from the array · never mix <= high with < size · take the element size from the array · translate the index in exactly one place, i - lo.
  10. The reflex loop is correct for [0:7] — measured — which is why it survives review, and why "safe default" is the wrong description of an assumption nothing can check.

8. Exercises

  1. Query low, high, size and element size for an array declared 3 to 10. Which two of those four would you have guessed wrong?
  2. Write both loops and run them over the same array. Record what the reflex loop returns and, separately, what it claims each value's index is.
  3. Of the three failures in §3, which could a tool report, and why can it report only that one?
  4. Run the reflex loop through a bare pointer instead of the API. What changes about the output, and what changes about your ability to notice?
  5. Write for (i = low; i < size; i++) and work out which elements it touches for a 3 to 10 array. Why is this the most dangerous of the three loops?
  6. Rewrite a loop body that uses a[i] so that the index translation happens exactly once. How many places did the translation appear before?
  7. Query the bounds of an array declared [0:7]. Explain, using §6, why the result is the reason the reflex loop keeps passing review.
  8. Write the two-line change that would make an existing zero-assuming model correct for any range. Would it have failed any existing test?

9. Summary

  • Ask first: low, high, size, bytes per element — measured 3, 10, 8, 4 for a 3 to 10 array.
  • size and low are independent facts, and no C array can be numbered from 3.
  • for (i = low; i <= high; i++) — high is the last index. Measured: all eight, round trip exact.
  • The reflex loop fails three ways at once — 3 rejected, 8, 9, 10 never read, five shifted by 3.
  • Only the out-of-range asks are reportable, and only because they went through the API.
  • Through a bare pointer nothing is reportable — measured, raw[0] is a real value under the wrong name.
  • The quiet counterpart to Chapter 21.4's loud mistake.
  • Four habits: bounds from the array · one bounds idiom, never a hybrid · element size from the array · translate the index once (i - lo).
  • Measured, the reflex loop is correct for [0:7] — the common case supplies the confirming evidence, as in 21.2 §5 and 20.9 §4.
  • Asking costs two calls, and the loop body is unchanged.

One dimension's bounds are now a solved problem, asked for rather than assumed. Adding a second dimension does not simply repeat the exercise, because the two languages disagree about something a single dimension never exposed: which index moves fastest through memory.

Standards & specifications

Governing standard
IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)

Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.