Skip to content

Phases 13 27 platform integration

Simon Dick edited this page Jun 20, 2026 · 1 revision

Phases 13–27 — Platform integration

Part of the Amiga port design log.

Runtime and platform-integration polish: REPL line editing, a dynamic heap, Pythonic VFS file I/O, Workbench launch, env-var integration, volume / assign introspection, AmigaDOS pattern matching, timer.device-backed timing, persistent REPL history, extra break signals, PROGDIR: on sys.path, and the build-variant matrix.

Two of the larger phases in this range have their own pages: Phase 17 — Native AmigaOS library access and Phase 18 — ARexx integration.

Phase 13 — Interactive REPL line editing ✅

shared/readline/readline.c drives the REPL. Cursor keys, history, kill/yank, Home/End all work.

CSI translation. AmigaOS's console.device emits cursor reports as single-byte CSI (0x9B) followed by parameters; shared/readline/ expects the two-byte ESC [ form. mp_hal_stdin_rx_chr() keeps a one-byte pending buffer: when FGetC returns 0x9B, return ESC and hand [ to the next call. Hosts that already emit ESC [ (vamos's xterm pass-through) see no change.

Running the REPL under vamos: tools/amiga-vamos-repl.sh puts the host TTY into -icanon -echo -isig for the duration of the run (vamos's SetMode(stdin,1) doesn't translate to tcsetattr on the host TTY). AMIGA_VARIANT=68040 selects the build.

Phase 14 — Dynamic heap growth ✅

Heap is a chain of AllocVec chunks managed via MICROPY_GC_SPLIT_HEAP_AUTO. Initial size from (priority order) -X heap=<N>[K|M], MICROPYHEAP env-var (dos.library GetVar), compile-time MICROPY_HEAP_SIZE. Cap via -X maxheap=<N> / MICROPYHEAPMAX.

main.c owns amiga_heap_chunks[16] for tracking (AllocVec is not reclaimed by AmigaOS on task exit, so all chunks must be FreeVec'd explicitly at shutdown). gc_get_max_new_split() reports AvailMem(MEMF_ANY|MEMF_PUBLIC|MEMF_LARGEST) minus a small headroom, clamped to -X maxheap. The GC's own sweep auto-releases empty grown chunks.

>>> amiga.heap_info()           # → (total_bytes, free_bytes, num_arenas)
(256000, 248000, 1)

Phase 15 — withdrawn

Originally exec.library memory pools. Phase 14's dynamic GC heap covers the common case; explicit native-buffer lifetimes can use alloc_vec. Number kept to avoid renumbering later phases.

Phase 16 — Pythonic file I/O via VFS ✅

MICROPY_VFS=1 + MICROPY_READER_VFS=1 with a port-local VfsAmiga in vfs_amiga.c. Stateless wrapper around Lock / Examine / CurrentDir / Open / Read / Write / CreateDir / DeleteFile / Rename. AmigaDOS keeps cwd in pr_CurrentDir. main() mounts a single VfsAmiga at /; AmigaOS-style paths (volume:dir/file or relative) route directly.

amigafile.c and amigaio.c are deleted — mp_lexer_new_from_file comes from extmod/vfs_reader.c; mp_import_stat is the inline that delegates to mp_vfs_import_stat; mp_builtin_open_obj is aliased to mp_vfs_open_obj.

vfs_amiga.c reuses the pr_WindowPtr = -1 requester-suppression pattern around every Lock. os.chdir keeps the first inherited cwd lock (it's the shell's, not ours) and UnLocks subsequent ones. ilistdir uses a finaliser so an abandoned for f in os.listdir(...) doesn't leak the directory lock. open(..., "r+b") correctly maps to MODE_OLDFILE (fail-if-missing), not MODE_READWRITE (create-on-missing).

Phase 19 — Workbench launch support ✅

WB-launched processes get a WBStartup message instead of argc/argv, and their config comes from .info tooltypes.

Detecting. Bebbo's crt0.o already does the heavy lifting — when pr_CLI == 0 it WaitPorts, GetMsgs the WBStartup, stashes the pointer in the global _WBenchMsg, and on exit Forbid()s and ReplyMsgs back to Workbench. We just extern struct WBStartup *_WBenchMsg; and test for null.

Console. WB-launched processes have pr_CIS/pr_COS both NULL, so main.c opens CON:0/30/640/200/MicroPython/AUTO/CLOSE/WAIT and points stdin/stdout/console-task at it. /AUTO defers window appearance until write; /WAIT keeps it open after main() returns.

Tooltypes. icon.library GetDiskObject(sm_ArgList[0].wa_Name); handle cached in IconBase + amiga_wb_diskobject. Two consumed at startup:

  • SCRIPT=<path> — script to run instead of REPL
  • HEAP=<N> / MAXHEAP=<N> — same parser as -X heap= (env vars still win, since they're more explicitly user-set)
if amiga.launched_from_workbench():
    extra = amiga.tooltype("EXTRA_PATH", "")
    for path in amiga.wb_selected_files():     # shift-clicked icons
        process(path)

wb_selected_files() renders each WBArg with NameFromLock + AddPart.

Cannot be exercised under vamos (no Workbench, no icon.library); full validation requires Amiberry/FS-UAE or real hardware.

Phase 20 — Env-var integration ✅

os.getenv / os.putenv / os.unsetenv via dos.library GetVar/SetVar with flags=0 (local CLI vars, falling through to global ENV:). Matches Unix os.putenv semantics — visible to child processes spawned via amiga.execute(), not to unrelated shells. For system-wide / persistent, write to ENV:/ENVARC: directly with open().

mpconfigport.h: MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1) + MICROPY_PY_OS_INCLUDEFILE "ports/amiga/modos.c". modos.c is #included by extmod/modos.c; no Makefile changes.

Vamos workarounds (both correct on real AmigaOS):

  1. GetVar returns 0 (not -1) for a missing variable; we treat len <= 0 as missing (misreports a genuine empty-string variable on real AmigaOS, correct on vamos).
  2. DeleteVar reads flags from D4, but the NDK fd specifies D2. We use the V36-documented "SetVar with NULL buffer deletes" form instead.

Phase 21 — Volume / assign introspection ✅

amiga.volumes()      # ['Python:', 'Ram Disk:', 'Workbench:']
amiga.assigns()      # {'C:': 'Workbench:C', 'LIBS:': 'Workbench:Libs', ...}
amiga.disk_info(path) # (free_bytes, total_bytes, block_size)

volumes()/assigns() walk LockDosList(LDF_VOLUMES/LDF_ASSIGNS | LDF_READ) via NextDosEntry. Assign targets from NameFromLock(dol_Lock) for DLT_DIRECTORY, or dol_misc.dol_assign.dol_AssignName for late/non-binding assigns. Multi-directory assigns report first dir only.

disk_info(path) locks (with pr_WindowPtr=-1), calls Info(), computes byte counts as uint64_t for >4 GB volumes. IoErr() mapped to MP_E* constants (e.g. ERROR_DEVICE_NOT_MOUNTEDMP_ENODEV).

Phase 22 — AmigaDOS pattern matching ✅

for path in amiga.match("S:#?"):       # eager list
for path in amiga.imatch("Work:#?.py"): # lazy iterator

One AnchorPath with trailing 512-byte buffer; ap_Strlen preset. MatchFirst parses the pattern (no separate ParsePattern). Empty list for ERROR_NO_MORE_ENTRIES / ERROR_OBJECT_NOT_FOUND; other DOS errors raise OSError.

imatch uses mp_type_polymorph_iter_with_finaliser; the iterator owns the AnchorPath, its finaliser calls MatchEnd + FreeVec so an abandoned loop (for p in amiga.imatch(...): break) doesn't leak. MatchFirst runs eagerly inside imatch() so the first result is ready on the first next(). Both suppress auto-requesters.

Phase 23 — timer.device-backed timing ✅

Replaced clock()-based path (busy-wait, ms-ish resolution) with:

  • mp_hal_delay_us(n)timer.device TR_ADDREQUEST via DoIO() for ≥200 µs, tight ReadEClock() busy-loop below.
  • mp_hal_delay_ms(n)timer.device for arbitrary-millisecond accuracy (the previous Delay() had 20 ms granularity).
  • mp_hal_ticks_us/ms()ReadEClock() (hardware counter, monotonic, cheap). EClock frequency cached at init.

Setup in amiga_timer.c: CreateMsgPort + CreateIORequest + OpenDevice("timer.device", UNIT_MICROHZ, ...) once at startup, before mp_init(). TimerBase (referenced by bebbo proto/timer.h inlines) is set from the request's io_Device. IORequest stored port-local-static; not thread-safe but the port is single-threaded.

Phase 24 — Persistent REPL history ✅

amiga_history.c loads / saves S:MicroPython.history (override via MICROPYHISTORY env-var). One entry per line, oldest first; CRLF tolerated on read. MICROPY_READLINE_HISTORY_SIZE bumped to 32 (was 8). Both paths suppress auto-requesters; failure (no S: volume, read-only, corrupt) is silent — history just doesn't persist that session.

main.c calls amiga_history_load() after mp_init() and amiga_history_save() before mp_deinit(). amiga.readline_history() and amiga.readline_push_history(line) exposed for scripting / testing.

Not in ENVARC: — that's for preferences; a frequently-written log there would make every reboot's copy ENVARC: ENV: all slower. S: is the conventional AmigaOS dotfile spot.

Phase 25 — Extra break signals ✅

amiga.signal(other_task_addr, amiga.SIGBREAKF_CTRL_E)
mask = amiga.wait_signal(amiga.SIGBREAKF_CTRL_D | amiga.SIGBREAKF_CTRL_E,
                         timeout_ms=5000)
  • signal(task_addr, sigmask)exec.library Signal(). NULL task raises ValueError.
  • wait_signal(mask, timeout_ms=None)Wait(). SIGBREAKF_CTRL_C is always ORed into the internal mask (so the user can break out) but always stripped from the return (so Ctrl+C never spuriously satisfies a user signal); if CTRL_C fires, KeyboardInterrupt.

timeout_ms uses a second, async timer.device IORequest in amiga_timer.c (kept separate from Phase 23's synchronous request so a pending Wait can't collide with an in-flight mp_hal_delay_us). SendIO arms the timer; the MsgPort's signal bit ORs into the Wait mask; the request is aborted/drained on return. If the second port fails at startup, wait_signal falls back to an untimed Wait — documented best-effort.

Phase 26 — PROGDIR: on sys.path

PROGDIR: is the auto-assign AmigaDOS creates per process for the executable's directory. main.c appends it to sys.path after mp_init() (prepending would lose it on every script run, since positional-script handling replaces sys.path[0] with the script's directory). After a script launch, sys.path is ['<script_dir>', '.frozen', 'PROGDIR:'] — script wins, PROGDIR: as final fallback.

Phase 27 — Build variants ✅

make VARIANT=<name>; build dir is build-<variant>.

Variant CPU Heap Notes
standard (default) -m68020 -msoft-float 256 KB Any 68020+, no FPU. Default for stock A1200 / unaccelerated 68030
68020fpu -m68020 -m68881 512 KB 68020/30 + 68881/2 (A2630, A3000). No libgcc soft-float wraps
68040 -m68040 1 MB 68040 built-in FPU (A3640, A4000/040)

Text-segment sizes: standard 356 KB, 68020fpu 330 KB, 68040 339 KB.

A500 isn't a target — its 68000 lacks unaligned access. Why 68040 is larger than 68020fpu: Motorola dropped transcendentals (FSIN/COS/TAN/ ATAN/etc.) from the 68040; gcc emits libm calls instead of inlines, pulling in ~9 KB. Alternative -m68040 -m68881 would shrink it back and rely on AmigaOS's FPSP (68040.library) to trap-and-emulate, but a binary that uses FSIN on a 68040 without FPSP loaded (custom Workbench, stripped startup, certain demos) gurus. The 9 KB is the price of self-containment.

sys.implementation._machine reports "Amiga with 68EC020" / "68020/68881" / "68040". floatconv.c pow/tgamma wraps apply on all variants (math-library bugs, not FPU codegen).

FPU variants require bebbo to have FPU multilibs (make all provides them); if m68k-amigaos-gcc -m68881 -print-multi-lib shows only a soft-float entry, the link fails with cannot find -lgcc.

Clone this wiki locally