This is an old revision of the document!


Subroutines

While you can use the GOSUB command in pair with RETURN to call parts of code as subroutines, the more sophisticated way of implementing subroutines is using the SUB … END SUB block.

Defining subroutines

Subroutines are named routines that accept zero or more arguments. The simplest syntax to define a subroutine is the following:

SUB <rountine_name> (arg1 AS <type>, arg2 AS <type>, ...)
  <statements>
END SUB

It is worth noting that a subroutine does not require arguments. In this case you still must add the empty parentheses after the routine name, though, like so:

SUB <routine_name> ()
  <statements>
END SUB

Calling subroutines

You can use the CALL keyword to call a subroutine. It behaves similarly to GOSUB with an important difference: CALL can pass arguments to the subroutine. Consider the following example:

SUB greet (name$ AS STRING * 10)
  PRINT "Hello, "; name$
END SUB

CALL greet("Emily") : REM will display: Hello, Emily
CALL greet("Mark") : REM will display: Hello, Mark

The CALL command will evaluate the argument list in the parentheses, pass all arguments to the subroutine and then instruct the computer to continue the program at the top of the subroutine.

Exiting subroutines

The subroutine will be exited at the END SUB statement. If you want to exit a subroutine earlier, use the EXIT SUB command:

SUB test (a AS INT)
  IF a < 0 THEN PRINT "positive number please" : EXIT SUB
  PRINT SQR(a)
END SUB
CALL test(-1)

Local and global variables

Variables defined inside a subroutine are local variables, i. e. they are only accessible within that subroutine. Global variables (the ones defined outside subroutines) are visible from within all subroutines.

globalvar = 1
SUB test ()
  PRINT globalvar : REM this is okay as globalvar is visible form here
  localvar = 5
END SUB
CALL test()
PRINT localvar : REM ERROR: localvar is not defined in the global scope

Shadowing

A local variable may have the same name as a global variable. In such cases the local variable will be used inside the subroutine. Consider the following example:

a = 42
SUB test ()
  a = 5
  PRINT a
END SUB
CALL test() : REM will output 5
PRINT a : REM will output 42

Static vs. dynamic

At this point it is important to understand how arguments can be passed to a subroutine. XC=BASIC offers two ways:

  • Dynamic arguments: the arguments are created dynamically in memory. Before the subroutine is called, a new area in memory - a stack frame - is allocated and this area holds the passed arguments. The advantage of dynamic memory allocation is that it allows recursive subroutine calls, i. e. the subroutine can call itself without messing up its data. However there is a penalty: dynamic arguments are operated much more slowlier.
  • Static arguments: the arguments live in a pre-allocated memory area. When the subroutine is called, the arguments are simply copied to this area. This is way much faster than dynamic frame allocation but it doesn't support recursion.

The default way of passing arguments is dynamic. If you'd like to pass arguments statically, append the STATIC keyword to the subroutine definition:

SUB <subroutine_name> (arg AS <type>) STATIC

Note

The STATIC keyword in a subroutine definition not only applies to its arguments but to all its local variables as well.

Warning

Always define your subroutines STATIC unless you intend to make recursive calls. The compiler will try to detect possible recursion and warn you about this in case you forget the STATIC keyword.

Static variables inside dynamic subroutines

You can mix static and dynamic behaviour using the STATIC keyword instead of DIM to mark local variables static when a subroutine is otherwise dynamic.

SUB test (arg AS INT)
  DIM a AS INT : REM a is dynamic
  STATIC b AS INT : REM b is static
END SUB

Note

Static local variables' values are preserved between subroutine calls. Upon entering a subroutine, static local variables have the same value as when the subroutine was last exited.

If a subroutine is defined as STATIC, all its local variables will be static, regardless whether you use the DIM or STATIC keyword to define them:

SUB test (arg AS INT) STATIC
  DIM a AS INT : REM a is static
  STATIC b AS INT : REM b is also static
END SUB

Warning

The stack frame that is allocated on each subroutine call must not be larger than 128 bytes. The compiler detects if a subroutine requires a larger stack frame, and emits a compile-time error in such cases. Therefore it is recommended to keep as many variables STATIC as possible.

Overloading

When passing arguments to a subroutine, the compiler will match the number of arguments in the CALL statement to the number of arguments in the subroutine declaration. If the number or arguments do not match, a compile-time error is shown.

If the number of arguments match, the compiler will compare the passed argument's type to what type the subroutine accepts. If the actual type can be converted to the accepted type, it will be silently converted. If however the types are not convertible (for example the subroutine accepts a numeric argument and you try to pass a string), compilation will fail with an error.

But what if you need subroutines that act differently when different number or type of arguments are passed? This is possible using overloading. A subroutine can have as many variations as you want. In this case the compiler will find the best match among the candidates for a call. For example:

SUB test (a AS INT) STATIC
  PRINT "a is an integer: "; a
END SUB

SUB test (a AS STRING * 16) OVERLOAD STATIC
  PRINT "a is a string: "; a
END SUB

CALL test(5)
CALL test("hello")

Warning

You have to use the OVERLOAD keyword when defining the second and subsequent overloaded variations of a subroutine. In this case the compiler will know that it's not by a mistake that you are using the same name.

Note

It is possible to overload the built-in XC=BASIC functions in your code, too.

Forward declaration

A subroutine can not be called before it was defined. This often makes it hard to organize your code in a clean and readable way. You may want to put subroutines at the end of your code and that's a perfectly valid requirement.

This is where forward declaration comes in handy. Forward declaration means that you declare a subroutine's all important properties (or the header of the subroutine) beforehands, and leave the actual implementation for later. The following example tries to illustrate this.

REM -- the top of the program
DECLARE SUB somesub (arg AS FLOAT) STATIC
REM -- the subroutine will be implemented later but it is already callable
CALL somesub(3.1415)
REM -- the bottom of the program
SUB somesub (arg AS FLOAT) STATIC
  PRINT "two times the argument is: "; arg * 2.0
END SUB

Warning

The implementation must use the same number and type of arguments as the declaration. Overloading is possible though: you can declare overloaded variations of the subroutine and imlement each variation later.

Subroutine visibility

Subroutines, as well as variables can also be defined with different visibility levels. XC=BASIC offers two options:

  • Global visibility: the subroutine is callable from within the entire code module where it was defined.
  • Shared visibility: the subroutine is callable from within all code modules.

Note

The default visibility for subroutines is global.

To define a subroutine as shared, append the SHARED keyword to its definition:

SUB <subroutine_name> (arg AS <type>) SHARED

END SUB

This way you make sure it is callable from within other code modules. Read more about Code Modules here.