Skip to content

Arrays

An array is one name for an ordered list of values, such as twelve monthly temperatures or an 8 by 8 game board. This page shows how to declare an array in d/BASIC, read and write its elements, ask it for its own bounds, resize it, pass it whole to a procedure, and use the three statements that act on a whole array at once: ARRAYCOPY, ARRAYFILL and ARRAYSWAP. If you have written BASIC before, subscripting works exactly the way you remember; the whole-array statements are the new part, and they exist so that a compiler and a very small runtime can do whole-array work well.

Declare an array

DIM Scores%(10)
DIM Temperatures(1 TO 12) AS DOUBLE
DIM Board(0 TO 7, 0 TO 7) AS INTEGER

Each dimension names either an upper bound on its own or an explicit lower TO upper pair. When you give only the upper bound, OPTION BASE supplies the lower one:

OPTION BASE 1
DIM Months$(12)          ' 1 TO 12
DIM Samples%(0 TO 255)  ' Explicitly zero-based

An array can have up to eight dimensions. Bounds and subscripts are converted to INTEGER.

Read and write elements

Board%(3, 4) = 9
PRINT Board%(3, 4)

An element behaves like a variable of the array's element type. When you assign to it, the value is converted to that type before it is stored.

Stay inside the declared bounds. d/BASIC checks every index; an out-of-range index stops the program with an error instead of quietly reading or writing unrelated memory.

Ask for the bounds

LBOUND and UBOUND report the array's current lower and upper bounds. Pass a second argument to ask about a particular dimension:

FOR Row% = LBOUND(Board%, 1) TO UBOUND(Board%, 1)
  FOR Column% = LBOUND(Board%, 2) TO UBOUND(Board%, 2)
    PRINT Board%(Row%, Column%);
  NEXT Column%
  PRINT
NEXT Row%

Without the dimension argument, dimension 1 is used.

Resize and erase

REDIM gives a dynamic array a new shape:

REDIM Samples%(0 TO NewLast%)

REDIM PRESERVE changes the bounds and keeps the values that fall inside both the old shape and the new one:

REDIM PRESERVE Samples%(0 TO NewLast%)

REDIM PRESERVE does not work on arrays of TYPE records (a TYPE is a record: a group of named fields that travels as one value; see Records & Objects). Use ordinary REDIM, which replaces the whole array in one step, or copy the record elements yourself when keeping them is part of your algorithm.

ERASE lets go of the array's storage and leaves the name empty:

ERASE Samples%

On this page, the link between an array's name and the storage that holds its elements is called its binding. After ERASE the binding is null: the name still exists but owns no storage at all. That is a real state of its own, not an empty one-dimensional array whose upper bound happens to be -1, and it matters to the whole-array statements below.

Whole-array parameters

Empty parentheses after a parameter name mean "the whole array", not one element:

DECLARE FUNCTION Sum& (Values() AS INTEGER)

FUNCTION Sum& (Values() AS INTEGER)
  DIM Total AS LONG
  FOR I% = LBOUND(Values) TO UBOUND(Values)
    Total = Total + Values(I%)
  NEXT I%
  Sum& = Total
END FUNCTION

The procedure sees the array's current bounds. A whole-array parameter is not an element, not a slice of the array, and not an untyped bag of values.

The three whole-array verbs

d/BASIC has three statements that act on a whole array at once. This page calls them verbs:

Goal Syntax What moves What gets allocated
copy ARRAYCOPY Source() TO Destination() every element, into storage of its own the destination becomes a fresh copy
fill ARRAYFILL Destination(), Value one converted value into every element same element type, dimensions and bounds; replaced all at once
swap ARRAYSWAP A(), B() only the two bindings no element is copied and no new array is allocated

Every operand written with () must be a bare whole array: A(), never A, A(I), Object.A() or a range of elements.

The three verbs accept arrays whose elements are numbers, variable-length STRINGs, or CLASS references (a CLASS is an object with an identity of its own; see Records & Objects). They do not accept arrays of TYPE records. Copy, initialize or exchange record elements yourself, one at a time, so that your source shows plainly where record values are being copied.

ARRAYCOPY: make an independent array

DIM Draft%(1 TO 4)
DIM Published%()

Draft%(1) = 10
Draft%(2) = 20
ARRAYCOPY Draft%() TO Published%()

Draft%(1) = 99
PRINT Published%(1)   ' 10: this is a separate binding

Use a copy when both arrays must go on changing independently of each other.

ARRAYFILL: one meaning, every element NEW

DIM Plane%(1 TO 4)
ARRAYFILL Plane%(), -1

DIM Names$(0 TO 2)
ARRAYFILL Names$(), "UNCLAIMED"

The value is converted exactly as one element assignment would convert it:

ARRAYFILL Plane%(), 2.6   ' each INTEGER receives 3

Fill keeps the destination's element type, number of dimensions and bounds. It is all-or-nothing: if the operation has to stop partway through its preparation, the old array stays in place unchanged rather than leaving you a half-filled one. Keeping that promise can require one temporary allocation the size of the array.

Filling an array you have ERASEd stops with an error, because there is no shape left to fill.

Why it is a language verb

This loop is readable:

FOR I% = LBOUND(Plane%) TO UBOUND(Plane%)
  Plane%(I%) = -1
NEXT I%

But ARRAYFILL Plane%(), -1 says more. It promises that the order of the elements does not matter, it lets the compiler's optimizer see the operation as a whole, it compiles to a single instruction in the compiled program file, .dbc, and it lets the d/OS runtime, the program that runs your compiled code, pick the best memory route on each machine while still filling the array all at once or not at all. (d/OS is the operating system that brings modern software to 8-bit home computers such as the Atari 800, the Commodore 64 and the Apple II.)

ARRAYSWAP: change ownership, not elements NEW

DIM Front%(1 TO 1000)
DIM Back%(1 TO 1000)

' Draw from Front while preparing the next state in Back.
ARRAYFILL Back%(), 0
' ...modify Back()...
ARRAYSWAP Front%(), Back%()

ARRAYSWAP exchanges the two bindings. Each array's contents, lower and upper bounds and number of dimensions travel with its binding. No elements are copied, allocated, kept or released.

The two arrays must have the same element type. When both hold storage, they must have the same number of dimensions; their bounds may differ:

DIM Live%(1 TO 3)
DIM Held%(0 TO 1)

ARRAYSWAP Live%(), Held%()
PRINT LBOUND(Live%); UBOUND(Live%)  ' 0  1

A null binding (one left empty by ERASE) is allowed, and swaps as null. That makes a fast "take this array and leave nothing behind" handoff possible:

DIM Ready%(1 TO 3)
DIM Taken%(1 TO 3)
ERASE Taken%

ARRAYSWAP Ready%(), Taken%()
' Taken now owns the former Ready array; Ready is null.

The memory trick to remember

Fill changes the values. Swap changes the name that owns them. If you find yourself copying a large back buffer just to make it the current one, you probably wanted ARRAYSWAP.

Classic scalar SWAP

SWAP exchanges two single values, or two records of compatible type:

SWAP Left%, Right%

It is deliberately different from ARRAYSWAP, whose operands are whole arrays.

Arrays of records and objects

TYPE Point
  X AS INTEGER
  Y AS INTEGER
END TYPE

DIM Path(0 TO 31) AS Point
DIM Buttons(0 TO 7) AS WorksButton

WorksButton is a button class from the widget library of d/Works, the office suite being written in d/BASIC (d/Write, d/Calc, d/Present). Record elements are stored inline, as values. Class elements are references to objects, and start out as NOTHING, the empty reference; assigning an object stores a counted reference to it. The difference between the two is explained in Records & Objects.

These are ordinary arrays, but the three whole-array verbs above apply only to arrays of numbers, variable-length strings and class references, not to arrays of TYPE records.

Patterns worth teaching

A ring without mysterious memory

NextSlot% = (NextSlot% + 1) MOD Capacity%
Samples%(NextSlot%) = Reading%

A table driven by bounds

FOR I% = LBOUND(Names$) TO UBOUND(Names$)
  PRINT Names$(I%)
NEXT I%

Do not repeat 1 TO 100 in every loop. Let the array report its own bounds.

Front/back state

Prepare the next state in Back(), then ARRAYSWAP Front(), Back(). Anyone reading Front() sees either the old complete state or the new complete state, never your half-built one.

Quick rules

  • Bounds are inclusive.
  • The default lower bound is 0 unless one module-level OPTION BASE 1 says otherwise.
  • Use LBOUND and UBOUND in procedures meant to be reused.
  • Use ERASE to release an array's storage.
  • Use ARRAYCOPY for independence, ARRAYFILL for a uniform state, and ARRAYSWAP to exchange ownership.
  • Use those three verbs only with arrays of numbers, variable-length strings or class references.
  • The parentheses on a whole array are not decoration; they tell the compiler you mean the whole array, not one element.

Next: Records & Objects →