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 rule, exactly as documented
Section titled “The rule, exactly as documented”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
- Read the file from the topIncluding any text pulled in by #include, which is merged before execution
- Find where the identifier first appearsFirst appearance, not first assignment
- First appearance was inside a function bodyThe name is LOCAL to that function
- First appearance was outside every functionThe name is GLOBAL
- A local or global declaration overrides bothIntroduced in AmiBroker 4.36
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.
The local keyword
Section titled “The local keyword”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.
The global keyword
Section titled “The global keyword”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.
The accidental global
Section titled “The accidental global”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.
Name collisions across include files
Section titled “Name collisions across include files”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.
A scope discipline
Section titled “A scope discipline”Five rules. They are not stylistic preferences; each one closes a specific failure above.
- Declare every variable a function assigns as
local, at the top of the function. Closes the accidental global. - 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.
- Use
globalonly to return a second value, and name it after the function. Closes the “where did this come from” problem. - Prefix every public name in a shared file. Closes all three collision types.
- Never call
Param()inside a library function.Paramadds 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.
Seeing it happen
Section titled “Seeing it happen”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 formula
Section titled “Complete formula”Complete runnable 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();How it works
Section titled “How it works”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.
Key functions
Section titled “Key functions”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 theCum, so the count would otherwise be Null.StrFormat( format, ... )builds the report line. Use%gfor numbers - the documentation is explicit that%ddoes not work, because AFL has no integers.
Expected result
Section titled “Expected result”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.
Test it
Section titled “Test it”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.
Common errors
Section titled “Common errors”| 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 |
Extension
Section titled “Extension”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
Sources for this lesson
4 verified · checked 2026-08-31
- 01AmiBroker User's Guide - User functions/procedures. Local/global scopeamibroker.com/guide/a_userfunctions.html2026-08-31
- 02AmiBroker User's Guide - Error and warning messagesamibroker.com/guide/errors2026-08-31
- 03AFL Function Reference - Status§ Low-level graphics example using local declarationsamibroker.com/guide/afl/status.html2026-08-31
- 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.