Pitfalls, Debugging, and Porting
The classic Lingo traps, debugging techniques, and rules for porting Lingo to and from modern languages.
Classic traps
1. Missing the
Built-in system properties are read with the. Omitting it silently creates a local variable:
set stageColor = 0 -- creates a LOCAL named stageColor. Does nothing.
set the stageColor = 0 -- sets the actual stage colorWhen old code "has no effect", check for a missing the first.
2. Parentheses on function calls
put rollover -- useless: names the function
put rollover() -- calls itAny call whose return value is used needs parentheses. count(list), random(6), the result idioms exist because Director 4 lacked general call syntax.
3. Lists are references
Passing a list to a handler passes the same list. Mutation leaks to the caller. Copy with duplicate(). See Lists.
4. getProp vs getaProp
getProp errors on a missing key; getaProp returns VOID. Old code depends on both behaviors deliberately.
5. Message routing surprises
A handler in a sprite behavior swallows the event for lower levels unless it calls pass. A keyDown handler on an editable field that does not pass eats the keystroke and the field never types. See Message hierarchy.
6. beginSprite limitations
Inside beginSprite, calculated sprite properties like rect may not be ready, and go, play, and updateStage are disabled. Initialization that needs geometry belongs in the first prepareFrame.
7. The itemDelimiter is global
Change it, and every item expression in every script sees the change until restored. Wrap and restore religiously.
8. and / or do not short-circuit
if objectP(x) and x.ready then still evaluates x.ready when x is VOID, and errors. Classic Lingo nests ifs instead.
9. Globals persist across movies
go to movie does not reset globals, the actorList, timeout objects, or open windows. Movies that misbehave on re-entry usually left state behind.
10. TRUE is 1, but any nonzero integer passes
if n then passes for any nonzero integer. But strings/lists/VOID in a condition are type errors, not falsy. Do not port JavaScript truthiness in either direction.
Debugging technique
put someExpression -- print to Message window
put "reached here", x, y -- multiple values
showGlobals() -- dump globals
showLocals() -- dump locals (inside a handler)
the traceScript = TRUE -- log executed lines
trace(expr) -- MX 2004 message-window output
alert "value:" && string(x) -- modal fallback- The Message window (Ctrl-M) evaluates any Lingo you type; it is a REPL against the running movie.
- Breakpoints + the Debugger/Watcher exist in the authoring tool; protected content has no source, so runtime
putlogging andalertremain the tools of last resort. the lastError-style diagnostics do not exist in classic Lingo; script errors surface as modal dialogs (authoring/projector) or silent halts (Shockwave withthe alertHookset).the alertHook: a script object whosealertHookhandler intercepts error dialogs; returning 1 suppresses them. Preservation runtimes should implement it, as shipped games use it to swallow errors.
Version differences that matter
| Area | Old (D4-D6) | New (D7-MX 2004) |
|---|---|---|
| Syntax | Verbose only (the x of sprite 1) | Dot syntax added; both valid |
| Continuation | ¬ character | \ in listings |
| Event stop | dontPassEvent | stopEvent() (dontPassEvent still parses) |
| Timers | the timer, the timeoutLength family | timeout objects |
| Text members | Fields (bitmap-era) | Fields + anti-aliased Text members (D7+) |
| Imaging | none | image object, copyPixels (D8+) |
| Integer coercion of bad strings | lenient (0) | strict (VOID) in MX 2004 |
| Windows | MIAW API | same + _player.windowList facades |
| Scripting objects | the globals | _movie, _player, _mouse, _key, _sound, _system |
When emulating a specific title, pin the player version it shipped against and test coercion, event, and text behaviors against that version's rules, not MX 2004's, where they differ.
Porting Lingo to modern languages
- Lingo is 1-based everywhere (strings, lists, channels, frames). Off-by-one bugs dominate naive ports.
repeat with i = 1 to nis inclusive on both ends.- No
null: the empty value is VOID;voidP()is the test. VOID propagates through arithmetic as errors, not NaN. - Property lists preserve insertion order; modern JS objects do too, but Python dicts pre-3.7 did not; order-dependent code exists.
- Case-insensitive identifiers: symbol keys
#Fooand#foocollide; two handlers differing only in case are the same handler. - String comparison is case-insensitive by default; port with explicit
lower()on both sides. - Integer division truncates;
modfollows the dividend's sign (C-style, not Python-style). value()is an expression evaluator; do not translate asparseInt.- Chunk expressions have no direct equivalent;
wordsplitting on any whitespace run anditemsplitting on a single-char delimiter need faithful reimplementation (empty items between consecutive delimiters are real items). - Sprite/score access has hidden state (puppeting, score authority per frame); a port that keeps game logic but replaces rendering must reproduce the event lifecycle or subtle logic breaks.
Reading decompiled Lingo
Decompilers emit mechanical but accurate source. Expect:
- Auto-named locals and loop variables where the name table was stripped selectively.
- Verbose syntax for property access even for MX-era movies (bytecode does not record which syntax was typed).
tell,meplumbing, andreturnstructure faithfully recovered; comments are gone forever.- Handler names always survive (they live in the name table, needed at runtime for message dispatch).
A fast orientation strategy for a decompiled game: list all movie scripts, find startMovie/prepareMovie for the boot path, grep for sendSprite/sendAllSprites/call( to map the messaging web, then grep global names to find the managers.