Skip to content
Level 3 · AFL DeveloperLessonPart 11 · page 2 of 526 min
26Minutes
5AFL functions
4Sources
StandardRequires
AFL functions taught here5

Variable Scope: local, global and the Traps

Here is a formula that works, and the same formula with two lines swapped, which does not. Nothing else has changed.

Fragment — not a complete formula

Threshold = 2.0;
function IsStretched( InputArray, Periods )
{
local Average;
Average = MA( InputArray, Periods );
return 100 * ( InputArray - Average ) / Average > Threshold;
}

Fragment — not a complete formula

function IsStretched( InputArray, Periods )
{
local Average;
Average = MA( InputArray, Periods );
return 100 * ( InputArray - Average ) / Average > Threshold;
}
Threshold = 2.0;

In the first version Threshold inside the function is the global you set to 2.0. In the second it is a local variable that nothing ever assigns, which raises Error 29, “Variable used without having been initialized”. Same characters, different order, different program.

That is not a quirk to memorise and move past. It is the single rule that decides how a formula and its functions share data, and it is the reason a library file needs a discipline rather than good intentions.

The User’s Guide states it in three sentences. Because AFL does not require variables to be declared, whether a name is local or global “depends on where it is FIRST USED”. If the identifier first appears inside a function definition, it is local to that function. If it first appears outside any function definition, it is global.

How AFL decides what a name means

  1. Read the file from the topIncluding any text pulled in by #include, which is merged before execution
  2. Find where the identifier first appearsFirst appearance, not first assignment
  3. First appearance was inside a function bodyThe name is LOCAL to that function
  4. First appearance was outside every functionThe name is GLOBAL
  5. A local or global declaration overrides bothIntroduced in AmiBroker 4.36
The decision is made once per identifier, by position in the merged text.

Two consequences follow immediately, and both are visible in the official example. The guide sets k = 4 at the top, defines function f( x ) which uses a local z and reads k, and then sets z = 5 at global level. It notes that k inside the function “references global variable k (first used above outside function)”, while the global z and the function’s local z are separate variables that do not interfere.

So a function can silently reach out and read a global, and a name can exist twice with two completely unrelated values.

local states that a name belongs to this function, whatever came before it in the file:

Fragment — not a complete formula

function ClampPeriod( RequestedPeriod )
{
local Result;
Result = RequestedPeriod;
if( Result < 2 ) Result = 2;
return Result;
}

You can declare several at once, comma-separated - the official low-level graphics example on the Status() page does exactly that:

Fragment — not a complete formula

local Miny, Maxy, pxchartbottom, pxchartheight;

Two things local does not do. It does not create a new scope inside a brace pair: the scope boundary in AFL is the function definition, not the block, so declaring local inside an if block is the same as declaring it at the top of the function. And it does not survive between calls - a local is created afresh each time the function runs.

global does the opposite: it says that a name used inside a function refers to a formula-level variable, even one that does not exist yet. The User’s Guide gives a specific reason for it - it “may be used to return more than one value from the function”.

Fragment — not a complete formula

function ClassifyStretch( InputArray, AveragePeriod, LimitPercent )
{
global ClassifyStretchPercent;
local Average;
local Result;
Average = MA( InputArray, AveragePeriod );
ClassifyStretchPercent = 100 * ( InputArray - Average ) / Average;
Result = ClassifyStretchPercent > LimitPercent;
return Result;
}

The caller gets the classification through return and the underlying percentage through ClassifyStretchPercent. This is legitimate and documented. It is also the only mechanism available, because arguments are passed by value and cannot carry a result back.

Use it sparingly, and name the variable after the function that produces it. A global called Percent is a landmine; one called ClassifyStretchPercent announces where it came from and is unlikely to collide with anything.

Now the failure mode. Suppose a formula uses a working variable called Scratch, and later includes a helper that also uses Scratch and forgets to declare it:

Fragment — not a complete formula

Scratch = 111;
function LeakyDouble( InputValue )
{
local Result;
Scratch = InputValue * 2; // this writes the GLOBAL Scratch
Result = Scratch;
return Result;
}

Scratch first appeared outside a function, so inside LeakyDouble it is the same variable. The function’s private working value has silently overwritten the caller’s. No error, no warning, and the damage appears far away from the cause - typically as a chart that is subtly wrong only when a particular helper happens to have been called.

The mirror-image failure is just as common and easier to spot, because it does produce an error. A helper that reads a configuration variable it never assigns works perfectly until someone moves the helper above the line that sets the variable - at which point the name first appears inside the function, becomes a local, and Error 29 arrives.

Everything above is about one file. #include makes it about all of them, because the preprocessor merges the included text into your formula before it runs. Two libraries that both use a global called Temp, or that both define function Trend(), are now in the same program.

Three distinct collisions are worth naming:

Function against function. Two files define the same function name. The second definition wins or the formula fails, and either way one library is no longer doing what its author wrote.

Function against variable. Defining a function whose name is already in use as a global variable raises Error 34, “Identifier already in use”. This is why a library that defines function Trend() will break the day a user writes Trend = Close > MA( Close, 200 ); above the include line.

Variable against variable. The quiet one. Two files use a global with the same name for different purposes, and whichever ran last determines the value.

The cure for all three is a prefix. Every public name in a library starts with the same short tag - Lib, Demo, your initials, whatever you like - so that collisions become impossible rather than unlikely. The library built at the end of this part prefixes everything with Lib for exactly this reason.

Five rules. They are not stylistic preferences; each one closes a specific failure above.

  1. Declare every variable a function assigns as local, at the top of the function. Closes the accidental global.
  2. Take every input as an argument. A library function that reads a global for configuration cannot be moved, cannot be tested in isolation, and breaks when the caller renames something. Closes the Error 29 ordering trap.
  3. Use global only to return a second value, and name it after the function. Closes the “where did this come from” problem.
  4. Prefix every public name in a shared file. Closes all three collision types.
  5. Never call Param() inside a library function. Param adds a control to the Parameters dialog of whichever chart called it, so a library that uses it silently injects parameters into every formula that includes it.

Applied together, these mean a library function’s behaviour depends on nothing except its arguments. That property is what makes the self-test in the project lesson meaningful: a function that reads a global cannot be tested, only observed.

Scope bugs are hard to believe until you watch one occur. This formula runs all four behaviours in a single chart and reports what happened in its title, so there is nothing to take on trust.

Complete runnable AFL

scope-demonstration.afl
// scope-demonstration.afl
// Part 11 - Variable Scope: local, global and the Traps
//
// GOAL
// Make AFL's scope rules visible instead of theoretical. Four functions sit
// in one formula, each demonstrating one documented behaviour, and the chart
// title reports what actually happened when they ran.
//
// WHAT TO WATCH
// The title reports the value of a global named Scratch before and after a
// helper that forgot to declare its working variable. If the two numbers
// differ, you have seen a function reach out of itself and overwrite a
// caller's variable - the single most expensive scope bug in AFL.
//
// ASSUMPTIONS
// - Any instrument, any interval. Nothing here is a trading rule.
_SECTION_BEGIN( "Scope demonstration" );
// ThresholdPercent is assigned OUTSIDE any function definition, so it is a
// global. It appears here, before every function below, which matters: AFL
// decides scope by where an identifier is FIRST USED.
ThresholdPercent = 2.0;
// Scratch is likewise a global, created before the functions that follow.
Scratch = 111;
// ---------------------------------------------------------------------------
// Case 1: a function that silently reads a global.
// Average, StretchPercent and Result are declared local. ThresholdPercent is
// not, and it was first used outside a function, so this line reads the global.
// The function works - and it now depends on a variable no caller can see in
// its signature.
// ---------------------------------------------------------------------------
function StretchAboveThreshold( InputArray, AveragePeriod )
{
local Average;
local StretchPercent;
local Result;
Average = MA( InputArray, AveragePeriod );
StretchPercent = 100 * ( InputArray - Average ) / Average;
Result = StretchPercent > ThresholdPercent;
return Result;
}
// ---------------------------------------------------------------------------
// Case 2: the same computation with every dependency in the argument list.
// This version can be moved into an include file and reused anywhere.
// ---------------------------------------------------------------------------
function StretchAboveLimit( InputArray, AveragePeriod, LimitPercent )
{
local Average;
local StretchPercent;
local Result;
Average = MA( InputArray, AveragePeriod );
StretchPercent = 100 * ( InputArray - Average ) / Average;
Result = StretchPercent > LimitPercent;
return Result;
}
// ---------------------------------------------------------------------------
// Case 3: the accidental global write.
// Scratch is not declared local here, and it already exists as a global, so
// this assignment writes to the caller's variable. Nothing warns you.
// ---------------------------------------------------------------------------
function LeakyDouble( InputValue )
{
local Result;
Scratch = InputValue * 2;
Result = Scratch;
return Result;
}
// ---------------------------------------------------------------------------
// Case 4: `global` used on purpose, which the User's Guide gives as the way to
// return more than one value from a function. The second result is named with
// the same prefix as the function, so it is obvious where it came from.
// ---------------------------------------------------------------------------
function ClassifyStretch( InputArray, AveragePeriod, LimitPercent )
{
global ClassifyStretchPercent;
local Average;
local Result;
Average = MA( InputArray, AveragePeriod );
ClassifyStretchPercent = 100 * ( InputArray - Average ) / Average;
Result = ClassifyStretchPercent > LimitPercent;
return Result;
}
AveragePeriod = Param( "Average period", 20, 2, 200, 1 );
// Case 3 in action: record the global, call the leaky helper, record it again.
ScratchBefore = Scratch;
LeakyResult = LeakyDouble( 5 );
ScratchAfter = Scratch;
// Cases 1 and 2 must agree, because the global and the argument hold the same
// number. The point is that only one of them says so in its signature.
StretchedByGlobal = StretchAboveThreshold( Close, AveragePeriod );
StretchedByArgument = StretchAboveLimit( Close, AveragePeriod, ThresholdPercent );
// Both arrays are Null over their warm-up bars, and Null propagates through
// every comparison and through Cum. Nz() turns the warm-up into an explicit
// zero so that the counter below reports a number rather than Null.
Disagreements = Cum( Nz( StretchedByGlobal, 0 ) != Nz( StretchedByArgument, 0 ) );
// Case 4 in action: the Boolean comes back through return, the percentage
// comes back through the declared global.
Classified = ClassifyStretch( Close, AveragePeriod, ThresholdPercent );
Plot( Close, "Close", colorDefault, styleCandle );
PlotShapes( IIf( Classified, shapeSmallCircle, shapeNone ), colorOrange, 0, High, 12 );
Plot( ClassifyStretchPercent, "Stretch above average, %",
colorBlue, styleLine | styleOwnScale );
_N( Title =
Name() + " - " + Interval( 2 ) + " - scope demonstration\n" +
StrFormat( "Global Scratch before LeakyDouble: %g\n", ScratchBefore ) +
StrFormat( "Global Scratch after LeakyDouble: %g (the helper wrote to it)\n",
ScratchAfter ) +
StrFormat( "LeakyDouble returned: %g\n", LeakyResult ) +
StrFormat( "Bars where the global version and the argument version disagree: %g\n",
LastValue( Disagreements ) ) +
"Stretch at the selected bar: " +
NumToStr( ClassifyStretchPercent, 1.2 ) + "%" );
_SECTION_END();

Download scope-demonstration.afl135 lines

The formula defines two globals, ThresholdPercent and Scratch, before any function. StretchAboveThreshold reads the first of them without declaring it - the documented “reaches out to a global” case. StretchAboveLimit takes the same number as an argument instead. LeakyDouble writes to Scratch without declaring it local. ClassifyStretch declares ClassifyStretchPercent as an explicit global and uses it to hand back a second result.

The main body records Scratch before and after calling LeakyDouble, runs both stretch functions and counts the bars on which they disagree, then plots the percentage that arrived through the declared global.

  • Cum( array ) accumulates a running total, used here to count disagreements across all bars in one array operation rather than a loop.
  • Nz( x, valueifnull ) replaces Null, NaN and infinity with a value you choose. Both stretch arrays are Null over their warm-up, and Null propagates through both the comparison and the Cum, so the count would otherwise be Null.
  • StrFormat( format, ... ) builds the report line. Use %g for numbers - the documentation is explicit that %d does not work, because AFL has no integers.

The title reports Scratch as 111 before the call and 10 after it. That single pair of numbers is the whole lesson: a function that was passed 5 and asked for nothing else has changed a variable in the calling formula. The disagreement count between the two stretch functions is 0, because the global and the argument hold the same number - the difference between them is not the answer, it is that only one of them declares what it depends on.

Change ThresholdPercent from 2.0 to 5.0 and confirm both stretch functions change together. Now move the line ThresholdPercent = 2.0; to the very bottom of the formula and re-verify: StretchAboveThreshold should now fail, because ThresholdPercent first appears inside the function and is therefore an uninitialised local. StretchAboveLimit is unaffected.

Then add local Scratch; as the first line of LeakyDouble and re-run. The before and after values in the title should now both read 111.

Symptom Cause
Error 29 on a variable you can see assigned The assignment is below the function that uses it, so the name became a local
A global changes value for no visible reason A function assigns to it without a local declaration
Error 34, identifier already in use A function was defined with the name of an earlier global variable
Two identical calls return different answers The function depends on a global that something else changed between them

Add a fifth function that declares local ThresholdPercent; and assigns 99 to it, then call it and confirm that the global is untouched - the documented “local and global of the same name are separate variables” case. Report both values in the title so the separation is visible rather than assumed.

AFL decides scope by where a name first appears in the merged text of your formula, which makes line order part of the semantics. local and global override that decision, and using them is not optional in code that other formulas will include. A function that declares its locals and takes its inputs as arguments behaves the same wherever it is pasted; one that does neither behaves differently depending on what happens to sit above it.

That is the property the next lesson depends on. Once code lives in an include file you no longer control what is above it.

Check your understanding

Question 1. Given this formula, what is the value of `x` after the call?
x = 5;

function Change()
{
    local x;
    x = 99;
}

Change();
Show the answer and why

Answer: 5

The local declaration creates a separate variable that happens to share a name. The User’s Guide is explicit that a local and a global with the same identifier do not interfere, so the global keeps its value.

Question 2. A function uses a variable named `Limit` that it never assigns, and no line above the function mentions `Limit`. What happens when the function runs?
Show the answer and why

Answer: Error 29, variable used without having been initialized

First appearance inside a function definition makes the name local. A local that is read before being assigned is uninitialised, which is Error 29 - the error is the good outcome here, because the alternative would be a silent wrong answer.

Question 3. Which of these are reasons to prefix every public name in a shared include file? Select all that apply.
Show the answer and why

Answer: A second library may define a function with the same name, A user formula may already use that name as a global variable, which raises Error 34, Two libraries may use a global of the same name for different purposes

The preprocessor merges every included file into one program, so all three collisions are real. AFL itself imposes no naming requirement - the prefix is a discipline that makes collisions impossible rather than merely unlikely.

Question 4. Why should a library function take its configuration as arguments rather than reading a global?
Show the answer and why

Answer: Its behaviour then depends only on the call, so it can be tested and moved without surprises

Reading a global works, and the official example shows it working. The objection is not that it fails but that the function’s result then depends on the text above it, which makes it untestable in isolation and fragile when the caller changes.

Sources for this lesson

4 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide - User functions/procedures. Local/global scopeamibroker.com/guide/a_userfunctions.html2026-08-31
  2. 02AmiBroker User's Guide - Error and warning messagesamibroker.com/guide/errors2026-08-31
  3. 03AFL Function Reference - Status§ Low-level graphics example using local declarationsamibroker.com/guide/afl/status.html2026-08-31
  4. 04AFL Function Reference -amibroker.com/guide/afl/_include.html2026-08-31

Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.