1.1 What are Subroutines?
A subroutine is a named, self-contained block of code that performs a specific task. Once defined, it can be called (invoked) from anywhere in the program — meaning you write the logic once and reuse it many times.
- Reusability: write the code once, call it from many places.
- Modularity: break a large program into smaller, manageable pieces.
- Readability: well-named subroutines act as abstractions — the reader sees what is being done without needing to read the details.
- Easier debugging & testing: each subroutine can be tested in isolation before being integrated.
- Abstraction: callers do not need to know how the subroutine works, only what it does.
CIE 9618 pseudocode has two kinds of subroutine:
Procedure
Performs an action (e.g. prints output, updates a value passed BYREF) but does not return a value.
CALL Greet("Sam")
Function
Performs a calculation and returns exactly one value via the RETURN statement.
Total <- Add(3, 4)
Key idea: the choice between a procedure and a function is determined by whether you need a value back. If the subroutine's job is to do something (print, update a flag, modify an array), use a procedure. If its job is to compute something (sum, max, average), use a function.
What are Subroutines?
1.2 Procedures: Declaration & Calling
A procedure is declared between PROCEDURE and ENDPROCEDURE. It is invoked with the CALL keyword.
Syntax:
PROCEDURE Name(Parameter1 : Type, Parameter2 : Type, ...)
// body — statements that perform the action
ENDPROCEDURE
// Calling the procedure:
CALL Name(Arg1, Arg2, ...)Example — a procedure that greets a user by name:
PROCEDURE Greet(Name : STRING)
OUTPUT "Hello, ", Name
OUTPUT "Welcome to Pseudocode Hub."
ENDPROCEDURE
// Main program
DECLARE User : STRING
INPUT User
CALL Greet(User)When CALL Greet(User) runs, the value of User is passed into the parameter Name. The two OUTPUT statements run, then control returns to the line after the CALL.
Example — a parameterless procedure:
PROCEDURE PrintDivider()
OUTPUT "------------------------"
ENDPROCEDURE
CALL PrintDivider()
OUTPUT "Report Title"
CALL PrintDivider()- Parameters are optional: empty parentheses
()are allowed when the procedure needs no input. - No RETURN statement: a procedure never uses RETURN to send back a value. (It can use the bare
RETURNkeyword to exit early, but this is rare at AS Level.) - Always call with CALL: writing
Greet("Sam")without CALL is a syntax error in 9618 pseudocode.
Warning: a procedure call is a statement, not an expression. You cannot write X <- MyProc(5) because MyProc returns nothing. Use CALL MyProc(5) as a standalone line, or change the procedure into a function.
Procedures: Declaration & Calling
1.3 Functions: Declaration & Calling
A function is declared between FUNCTION and ENDFUNCTION. It must declare the type of value it returns (using RETURNS) and must contain at least one RETURN statement.
Syntax:
FUNCTION Name(Parameter1 : Type, ...) RETURNS ReturnType
// body — compute the result
RETURN Value // mandatory — sends Value back to the caller
ENDFUNCTION
// Calling the function — use it in an expression:
DECLARE Result : ReturnType
Result <- Name(Arg1, ...)Example — a function that adds two integers:
FUNCTION Add(A : INTEGER, B : INTEGER) RETURNS INTEGER
DECLARE Sum : INTEGER
Sum <- A + B
RETURN Sum
ENDFUNCTION
// Main program
DECLARE Result : INTEGER
Result <- Add(3, 4)
OUTPUT Result // Outputs: 7Functions can also be used directly inside expressions — there is no need to assign them to a variable first:
OUTPUT Add(10, 20) // Outputs: 30
DECLARE Total : INTEGER
Total <- Add(2, 3) + Add(4, 5) // Total = 5 + 9 = 14
IF Add(X, Y) > 100 THEN
OUTPUT "Big"
ENDIF- Mandatory RETURN: every function must execute a RETURN that sends back a value of the declared type. Forgetting RETURN is a compile-time error.
- Exactly one value: a function returns exactly one value. To send back multiple pieces of information, use several
BYREFparameters on a procedure, or return an array. - No CALL keyword: functions are called by writing their name and arguments in an expression. The result is then used like any other value.
- Multiple RETURN points are allowed but a single RETURN at the end is generally easier to read and debug.
Pattern: the RETURN statement does two things at once — it sends a value back to the caller and it immediately terminates the function. Any code after RETURN inside the same block is unreachable. A single RETURN at the end of the function is the cleanest pattern.
Functions: Declaration & Calling
1.4 Parameters & Arguments
- Formal parameters are the named variables listed in the subroutine header — e.g.
PROCEDURE Greet(Name : STRING)—Nameis the parameter. - Actual arguments are the concrete values supplied at the call site — e.g.
CALL Greet("Sam")—"Sam"is the argument. - The number, order and type of arguments must match the parameters exactly.
CIE 9618 pseudocode supports two passing mechanisms: BYVALUE (the default for scalars) and BYREF (by reference).
BYVALUE (default)
PROCEDURE Double(X : INTEGER)
X <- X * 2
ENDPROCEDURE
DECLARE Num : INTEGER
Num <- 5
CALL Double(Num)
OUTPUT Num // 5 — caller's Num unchangedBYREF (by reference)
PROCEDURE Double(BYREF X : INTEGER)
X <- X * 2
ENDPROCEDURE
DECLARE Num : INTEGER
Num <- 5
CALL Double(Num)
OUTPUT Num // 10 — caller's Num changed!How BYVALUE works: the value of the argument is copied into the parameter. The subroutine works on its own private copy. Changes to the parameter inside the subroutine do not affect the caller's variable. This is the safe default.
How BYREF works: instead of copying a value, the subroutine receives a reference (the memory address) to the caller's actual variable. Assigning to the parameter inside the subroutine modifies the caller's variable directly. This is useful when a subroutine needs to send back more than one result, or when working with large data structures.
Side-by-side trace — calling Double(Num) where Num = 5
Arrays are passed BYREF by default in 9618 pseudocode — even without writing the BYREF keyword. This is for efficiency (no need to copy every element) and means any changes the subroutine makes to array elements will be visible to the caller after the call returns.
PROCEDURE FillZeros(BYREF Arr : ARRAY[1:5] OF INTEGER)
DECLARE I : INTEGER
FOR I <- 1 TO 5
Arr[I] <- 0
NEXT I
ENDPROCEDURE
DECLARE Data : ARRAY[1:5] OF INTEGER
CALL FillZeros(Data)
// Data is now [0, 0, 0, 0, 0] — the array was modified directly.Rule of thumb: use BYVALUE when the subroutine only needs to read the input. Use BYREF when the subroutine needs to modify the caller's variable (or when you need to send back more than one result, since functions return only one value).
Parameters & Arguments
1.5 Local vs Global Variables
Every variable in a 9618 program has a scope — the region of the program where the variable is visible and can be used.
- Local variables: declared inside a subroutine using
DECLARE. Visible only inside that subroutine. Created when the subroutine is called, destroyed when it ends (lifetime = duration of the call). - Global variables: declared at the top of the program (outside any subroutine) using
GLOBAL. Visible from anywhere in the program. Live for the entire run of the program.
GLOBAL Counter : INTEGER // Global — visible everywhere
Counter <- 0
PROCEDURE Increment()
DECLARE Step : INTEGER // Local — visible only inside Increment
Step <- 1
Counter <- Counter + Step // Can read AND write the global
ENDPROCEDURE
CALL Increment()
CALL Increment()
OUTPUT Counter // 2
// OUTPUT Step // ERROR — Step is local to Increment, not visible hereWhy local variables are preferred:
- Avoid side-effects: a local can only be changed by its own subroutine, so you cannot accidentally break another part of the program.
- Prevent name collisions: two subroutines can both have a local called
CountorTempwithout any conflict. - Save memory: locals only exist while their subroutine is running; the memory is freed when the subroutine ends.
- Easier to test & reuse: a subroutine that only uses its parameters and locals can be lifted into another program without dragging globals along.
Shadowing: if a local variable is declared with the same name as a global, the local "shadows" the global inside that subroutine — references use the local, and the global is untouched. Shadowing is legal but confusing; avoid it by using distinct names.
GLOBAL X : INTEGER
X <- 100
PROCEDURE Confuse()
DECLARE X : INTEGER // Local X shadows the global X
X <- 5 // Changes the LOCAL X, not the global
OUTPUT X // 5
ENDPROCEDURE
CALL Confuse()
OUTPUT X // 100 — the global was never changedWarning: globals make debugging hard. If a global is changed by a call to ProcA, and ProcA calls ProcB which also uses the global, the order of calls suddenly matters in surprising ways. Prefer parameters and return values over shared globals.
Local vs Global Variables
1.6 Built-ins, Stepwise Refinement & Pitfalls
Built-in functions are pre-defined by the language — you do not declare them, just call them. Common 9618 built-ins include:
// String built-ins
LENGTH("Hello") // 5 — number of characters
UPPER("Hello") // "HELLO"
LOWER("HELLO") // "hello"
SUBSTRING("Hello", 2, 3) // "ell" (start position, length)
// Numeric built-ins
ROUND(3.14159, 2) // 3.14 — round to 2 decimal places
RANDOM(6) // random integer in range 1..6
13 MOD 5 // 3 — remainder (operator, not function)
13 DIV 5 // 2 — integer quotient (operator, not function)Contrast these with user-defined functions — the ones you write yourself using FUNCTION. Both are called the same way (used in an expression), but built-ins are always available without any declaration.
Stepwise refinement (also called top-down design) is the standard method for tackling large problems. Start with the whole problem, split it into a few major sub-tasks, then keep splitting each sub-task until every piece is small enough to implement as a single subroutine. Subroutines are the natural unit of decomposition.
Worked example — design a program that reads 10 student marks and prints the average and the highest mark. Top-level decomposition:
// Top level: main program reads input, then reports results
DECLARE Marks : ARRAY[1:10] OF INTEGER
ReadMarks(Marks) // Procedure — fills the array
DECLARE Avg : REAL
Avg <- CalculateAverage(Marks) // Function — returns the mean
DECLARE Best : INTEGER
Best <- FindHighest(Marks) // Function — returns the max
OUTPUT "Average = ", Avg
OUTPUT "Highest = ", BestEach subroutine can now be written and tested independently. The main program reads almost like English — that is the power of well-named subroutines combined with stepwise refinement.
Common pitfalls to avoid:
- Forgetting RETURN in a function — the function declares
RETURNS INTEGERbut never executes a RETURN statement. Compile-time error. - Mismatched parameter count or types — calling
Add(3)when the function expects two parameters, or passing a STRING where an INTEGER is required. - Shadowing globals with locals — a local with the same name as a global hides the global inside the subroutine, leading to surprising behaviour. Use distinct names.
- Accidentally mutating caller data via BYREF — passing BYREF when BYVALUE would do means the subroutine silently modifies the caller's variable. Default to BYVALUE; use BYREF only when you intend to mutate.
- Using a procedure like a function — writing
X <- MyProc(5)whereMyProcis a procedure. Procedures return nothing, so this is illegal. UseCALL MyProc(5)or changeMyProcinto a function. - Forgetting that arrays are passed BYREF by default — modifications to array elements inside the subroutine are visible to the caller afterwards. If you need to keep the original array intact, pass a copy.
Key points summary — a quick recap of everything you need to remember about Procedures & Functions at AS Level:
Remember: a procedure performs an action (called with CALL); a function computes and returns one value (used in an expression, must contain RETURN). BYVALUE copies (default for scalars), BYREF aliases (default for arrays). Locals are safer than globals. Subroutines are the building blocks of stepwise refinement.
Built-ins, Stepwise Refinement & Pitfalls
1.7 RETURN Inside a Loop — Instant Exit
The RETURN statement does two things at once: it sends a value back to the caller and it immediately terminates the function. Crucially, this happens even if RETURN is executed inside a loop — the loop does not continue, and any code after the RETURN (inside or outside the loop) is never executed.
Important rule: any code written after a RETURN statement — whether inside or outside a loop — is not executed. RETURN is an instant exit.
Example — find the first even number in an array and return it immediately:
FUNCTION FindFirstEven(NumberList : ARRAY[1:5] OF INTEGER) RETURNS INTEGER
DECLARE i : INTEGER
FOR i <- 1 TO 5
IF NumberList[i] MOD 2 = 0 THEN
RETURN NumberList[i] // exits the function HERE
ENDIF
NEXT i
RETURN -1 // only runs if NO even number was found
ENDFUNCTION
DECLARE Numbers : ARRAY[1:5] OF INTEGER
Numbers[1] <- 3
Numbers[2] <- 7
Numbers[3] <- 8
Numbers[4] <- 5
Numbers[5] <- 2
DECLARE Result : INTEGER
Result <- FindFirstEven(Numbers)
OUTPUT Result // Outputs: 8Step-by-step trace:
i = 1:3 MOD 2 = 1→ not even, continue.i = 2:7 MOD 2 = 1→ not even, continue.i = 3:8 MOD 2 = 0→ even!RETURN NumberList[3]runs.- The function instantly stops. Value
8is returned. - The loop does not check
i = 4ori = 5. - The final
RETURN -1is also ignored — it only runs if the loop finishes without finding an even number.
When is this useful? When you only need the first matching item and want to stop early to save time. The rest of the loop is unnecessary once the condition is met. The pattern RETURN value inside the loop plus RETURN sentinel after the loop is the standard "find first or report not-found" structure.
RETURN Inside a Loop — Instant Exit
1.8 String Functions Inside User-Defined Functions
In Paper 2 you are often asked to write a FUNCTION that processes a string — extract part of a string, validate a code, format output, or convert case. To do this you combine a user-defined function with the built-in string functions provided by 9618 pseudocode.
Common 9618 built-in string functions:
| Function | Returns | Example |
|---|---|---|
LEFT(s, n) | First n characters | LEFT("Computer", 3) → "Com" |
RIGHT(s, n) | Last n characters | RIGHT("Computer", 3) → "ter" |
MID(s, start, len) | len chars from position start | MID("Computer", 4, 3) → "put" |
LENGTH(s) | Number of characters | LENGTH("Computer") → 8 |
TO_UPPER(s) | Uppercase version | TO_UPPER("abc") → "ABC" |
STR_TO_NUM(s) | String converted to number | STR_TO_NUM("20") → 20 |
NUM_TO_STR(n) | Number converted to string | NUM_TO_STR(25) → "25" |
Example 1 — extract the first 3 characters using LEFT:
FUNCTION GetPrefix(Text : STRING) RETURNS STRING
DECLARE Prefix : STRING
Prefix <- LEFT(Text, 3)
RETURN Prefix
ENDFUNCTION
OUTPUT GetPrefix("Computer") // Outputs: ComExample 2 — extract the month from a date "DD/MM/YYYY" using MID:
FUNCTION GetMonth(DateValue : STRING) RETURNS STRING
DECLARE Month : STRING
Month <- MID(DateValue, 4, 2) // position 4, length 2
RETURN Month
ENDFUNCTION
OUTPUT GetMonth("25/12/2025") // Outputs: 12Example 3 — validate a password using LENGTH (returns BOOLEAN):
FUNCTION CheckPassword(Password : STRING) RETURNS BOOLEAN
IF LENGTH(Password) >= 8 THEN
RETURN TRUE
ELSE
RETURN FALSE
ENDIF
ENDFUNCTION
OUTPUT CheckPassword("abc") // Outputs: FALSE
OUTPUT CheckPassword("mypassword") // Outputs: TRUEExample 4 — combining LENGTH, MID and LEFT to get initials from "Ali Khan":
FUNCTION GetInitials(FullName : STRING) RETURNS STRING
DECLARE FirstInitial : STRING
DECLARE SpacePosition : INTEGER
DECLARE SecondInitial : STRING
DECLARE i : INTEGER
SpacePosition <- 0
// Find the position of the space character
FOR i <- 1 TO LENGTH(FullName)
IF MID(FullName, i, 1) = " " THEN
SpacePosition <- i
ENDIF
NEXT i
FirstInitial <- LEFT(FullName, 1)
SecondInitial <- MID(FullName, SpacePosition + 1, 1)
RETURN FirstInitial & SecondInitial
ENDFUNCTION
OUTPUT GetInitials("Ali Khan") // Outputs: AKExample 5 — convert a string number, add 5, return a string (STR_TO_NUM + NUM_TO_STR):
FUNCTION IncreaseNumber(Value : STRING) RETURNS STRING
DECLARE NumberValue : INTEGER
DECLARE Result : STRING
NumberValue <- STR_TO_NUM(Value) // "20" -> 20
NumberValue <- NumberValue + 5 // 20 + 5 = 25
Result <- NUM_TO_STR(NumberValue) // 25 -> "25"
RETURN Result
ENDFUNCTION
OUTPUT IncreaseNumber("20") // Outputs: 25Pattern: convert STRING to INTEGER before arithmetic, and INTEGER to STRING before returning a string. You cannot do maths on a STRING, so STR_TO_NUM is required first; if the function signature says RETURNS STRING, the final value must be a string, so NUM_TO_STR is required last.
String Functions Inside User-Defined Functions
1.9 Text Files with Procedures & Functions
A common AS Level Paper 2 task is to wrap file operations inside a procedure (when no value needs to come back) or a function (when you must return a count, a result, or a TRUE/FALSE). The file commands are OPENFILE, WRITEFILE, READFILE, CLOSEFILE and EOF().
The three file modes:
FOR WRITE— creates a new file (or overwrites an existing one; old data is deleted).FOR APPEND— adds new data to the END of an existing file; original content is preserved.FOR READ— opens the file for input; use withWHILE NOT EOF(FileName).
Example 1 — a PROCEDURE that creates a file and writes one line (FOR WRITE):
PROCEDURE CreateFileAndWrite(FileName : STRING, Data : STRING)
OPENFILE FileName FOR WRITE
WRITEFILE FileName, Data
CLOSEFILE FileName
ENDPROCEDURE
CALL CreateFileAndWrite("Test.txt", "Hello World")Example 2 — a PROCEDURE that appends a line to an existing file (FOR APPEND):
PROCEDURE AppendToFile(FileName : STRING, NewData : STRING)
OPENFILE FileName FOR APPEND
WRITEFILE FileName, NewData
CLOSEFILE FileName
ENDPROCEDURE
CALL AppendToFile("Test.txt", "Second Line")Example 3 — a PROCEDURE that reads and displays every line (FOR READ + EOF loop):
PROCEDURE DisplayFile(FileName : STRING)
DECLARE LineData : STRING
OPENFILE FileName FOR READ
WHILE NOT EOF(FileName)
READFILE FileName, LineData
OUTPUT LineData
ENDWHILE
CLOSEFILE FileName
ENDPROCEDURE
CALL DisplayFile("Test.txt")Example 4 — a FUNCTION that counts the records in a file (returns a value, so it must be a function):
FUNCTION CountLines(FileName : STRING) RETURNS INTEGER
DECLARE LineData : STRING
DECLARE Counter : INTEGER
Counter <- 0
OPENFILE FileName FOR READ
WHILE NOT EOF(FileName)
READFILE FileName, LineData
Counter <- Counter + 1
ENDWHILE
CLOSEFILE FileName
RETURN Counter
ENDFUNCTION
DECLARE Total : INTEGER
Total <- CountLines("Log.txt")
OUTPUT "Records: ", TotalExample 5 — early-exit search in a SORTED file (combines RETURN-in-a-loop with file reading):
// File Stock.txt is sorted in ascending order of ItemNum.
// Return TRUE if the item is NEW (not already in the file),
// FALSE if it already exists.
FUNCTION CheckNewItem(LineData : STRING) RETURNS BOOLEAN
DECLARE FileName : STRING
DECLARE NewItemNum : STRING
DECLARE FileLine : STRING
DECLARE FileItemNum : STRING
FileName <- "Stock.txt"
NewItemNum <- LEFT(LineData, 4)
OPENFILE FileName FOR READ
WHILE NOT EOF(FileName)
READFILE FileName, FileLine
FileItemNum <- LEFT(FileLine, 4)
IF FileItemNum = NewItemNum THEN
CLOSEFILE FileName
RETURN FALSE // found — stop early
ENDIF
IF FileItemNum > NewItemNum THEN
CLOSEFILE FileName
RETURN TRUE // passed it — stop early
ENDIF
ENDWHILE
CLOSEFILE FileName
RETURN TRUE // not found anywhere
ENDFUNCTIONWhy sorted files are efficient: because the records are in ascending order, as soon as the current record key is greater than the target you know the target cannot appear later. The function can CLOSEFILE and RETURN early without reading the rest of the file — fewer comparisons, faster search.