Native Lingo Internals
The recovered Dirapi.dll scripting machinery - symbol interning, the API registry, property descriptor records, executor branch families, and native error codes.
Overview
Inside Dirapi.dll, the scripting surface (the ticks, the locH of sprite 1, the font of member 1...) is not a hardcoded switch statement. It is a generic descriptor machine:
symbol string -> interned symbol ID -> registry lookup -> descriptor record
-> shared descriptor executor
-> emitted VM operationsEverything below is native code evidence unless noted (see evidence levels).
Symbol bootstrap and interning
Two string tables seed the symbol space at startup:
- A fixed bootstrap table at
68148894(core names:mouseUp= ID0x39,sprite=0x2e,member=0x58, chunk nameschar/word/item/line=0x20-0x23, ...). - A caller-provided Director term table at
68140e04(loaded second:movie,puppet,ink,foreColor, ...).
The interning helper (FUN_680c1f63) hashes each length-prefixed string; if the name already exists it returns the existing ID, otherwise it assigns the next sequential ID. Consequences:
- Second-table row numbers are NOT symbol IDs once any duplicate has occurred.
- Example: ID 57 is
mouseUp(from the fixed table), even though the second table's row 57 isbeepOn;fontlands at ID 751 only after earlier duplicates were skipped. - Aliases intern to one ID family by sharing descriptors instead:
textStyle/fontStyle,textFont/font,textHeight/lineHeight,textAlign/alignment,textSize/fontSizepairs each register the same descriptor pointer under two IDs: the D4-era name and the D7+ name are literally the same property natively.
The API registry
A registry helper (FUN_680cba3c) stores descriptor pointers keyed by symbol ID:
slot = FUN_680dc6c0(table, symbolId, 1, 0); // find-or-create row
*slot = descriptorPtr;Two registrar functions make 169 registration calls total (44 + 125), covering the classic global property/function surface: ticks, memorySize, floatPrecision, primary handler scripts, date/time, coercion functions (integer, float, value, string, symbol), type predicates, sprite properties (locH, locV, ink, blend, foreColor, backColor, width, height, rect, stretch, puppet, cursor, moveableSprite, editableText...), text properties, timeout family, stage metrics (stageLeft/Right/Top/Bottom), mouse state (mouseH/V, clickOn, doubleClick, mouseLine/Item/Word/Char, mouseCast/Member), and system flags (colorDepth, exitLock, netPresent, safePlayer...).
All 169 rows were decoded: symbol ID, name, source table, and descriptor pointer. The registry is the bridge every the x expression crosses.
Dispatch route
- Opcode
0x1fof the script VM is registered to a wrapper (FUN_680e5680) that: reads a VM value that must be a symbol (type tag 8), looks the ID up in the registry (lookup-only), and calls the shared executorFUN_680c8c3ewith the registered descriptor. No descriptor found = fallback route (FUN_680c8b4c). - A parallel route (
FUN_680e3e1a) handles set/mutation, calling the same executor with a setter flag.
Descriptor records
Descriptor pointers are data records, mostly 4-cell (16-byte) rows, sometimes chained 3-cell groups. First cell = flag word. Observed distribution across the 160 unique descriptors:
| First word | Count | Family |
|---|---|---|
0x40 | 79 | Simple global/event/property (read via direct route). |
0x415 | 32 | Sprite-targeted property (... of sprite n). |
0x410 | 31 | Global writable property. |
0x315 | 4 | Member/cast/script text-style chains. |
0x20, 0xd, 0x15, 0x215, 0x615, 0x26, 0x7 | 12 | Small special families (last, number, text, hilite, name, date formats...). |
Examples (confirmed cells):
mouseUp 6814f9a8: [0x40, 0x39, 0x0, 0x0] ; 0x39 = symbol ID of mouseUp
ticks ........: [0x40, 0x12b, 0x0, 0x0]
locH 6814f3b0: [0x415, 0x2e, 0x60d, 0x0] ; 0x2e = "sprite"
ink 6814f420: [0x415, 0x2e, 0x60a, 0x0]
blend 6814f450: [0x415, 0x2e, 0x61c, 0x0]
font 6814f850: [0x315, 0x58, 0x904, 0x315, 0x59, 0x904, 0x315, 0x7b, ...]
; 0x58 member, 0x59 cast, 0x7b scriptThe third cell of a target-property descriptor is a packed payload, split by the executor: low byte = property index delivered to the target object, next byte = route/type byte. So ink (payload 0x60a) becomes route 0x06, channel property index 0x0a; blend = 0x06/0x1c; locH = 0x06/0x0d.
Executor branch families
FUN_680c8c3e is the shared descriptor executor. Its confirmed flag bits:
| Bit | Effect |
|---|---|
0x1 | Read and match a target symbol (e.g. sprite, member). Mismatch = error 0x15. |
0x4 | Consume/evaluate the target expression (sprite 1) before emitting. |
0x8 | Marks continued target groups in chains (under study). |
0x10 | Setter mode allowed; setter requires to or = next (error 0x0e on bad RHS syntax). |
0x100 | Enables chunk-qualifier route: accepts char/word/item/line (0x20-0x23). |
0x200 | Checks for an of castLib form. |
0x40 | Simple direct branch (below). |
Branch behavior:
0x40simple: if not followed by anof-target form, emits op0x43, resolves the descriptor's second cell, emits op0x66; rejects setter mode with error0x14. With anoftarget, it enters the shared target path emitting0x61(get) /0x62(set).0x415sprite property: matchsprite, evaluate the sprite expression, split payload (low byte as immediate via the constant emitter, next byte as operand to the operation emitter), emit0x5c(get) /0x5d(set).0x410global writable: same payload split and0x5c/0x5dops, but no target symbol and no target expression.0x315text chains: iterate 3-cell groups (flags, target symbol, payload) coveringmember/cast/scripttargets with optional chunk qualifiers; e.g.the font of word 2 of member 1walks this chain.
Helper semantics (confirmed): a VM value reader with push-back (probe-then-restore) support; a symbol reader accepting only of/in (0x3e/0x5f); a setter-syntax reader accepting only to/=; an operation emitter with byte/word/dword operand-width promotion; an immediate-constant emitter with dedicated zero/byte/word/dword encodings; and an output writer maintaining depth counters.
Native error codes
| Code | Condition |
|---|---|
0x0e | Setter right-hand-side syntax invalid. |
0x12 | Expected of/in form missing. |
0x13 | Top-level value was not a symbol. |
0x14 | Setter attempted on a read-only descriptor branch. |
0x15 | Target symbol match failed in a descriptor chain. |
What this means for emulators
- Implement the descriptor model, not string tables. The native design is: intern symbol, look up descriptor, interpret flags, match targets, split payloads, emit get/set ops. Emulators that hardcode per-name behavior drift on aliases (
textFontvsfont) and on chunk-qualified forms. - Aliases are free when descriptors are shared, exactly as the native tables do.
- Payload property indexes are the channel contract: sprite
inkis channel property0x0a,blendis0x1c; the channel object's getter/setter/dirty routes consume those indexes (rendering bridge). - Still open: full semantics of payload high bytes in
0x315chains (0x903,0x904,0x907), the meaning of emitted ops0x43/0x61/0x62/0x66beyond their roles, and chain termination rules. Treat unproven cells as research targets, not implementation guidance.