- Context: Scripts want interpolated strings. Full
str.format/ format-map / nested f-strings / debug=conflict with Level constraints (no kwargs, limitedformat()). - Decision: Accept
f/F-prefixed one-line quotes asPY68_TOKEN_FSTRING. Parse toJOINED_STRof literalSTRINGparts andFORMATTED_VALUE{expr[!s|!r|!a][:spec]}with literal format specs matching builtinformat(). Compile without new opcodes:str/repr/ascii/formatcalls plusOP_ADD.{{/}}are literal braces. Rejectfr/rf/b/uprefixes, nested f-strings, empty{}, and expressions in format specs. - Alternatives considered:
BUILD_STRINGopcode; full CPython f-string tokenizer; defer untilstr.formatmethods exist. - Consequences: Updates D-0025 / D-0039 to allow this subset. Bare
fremains a normal name.
- Context: Scripts need CPython-style
iter/nextwithout classes or user__iter__.foralready converts containers toPY68_OBJECT_RANGEviaRANGE_INIT/RANGE_NEXT. - Decision: Expose that cursor as the iterator.
iter(x)(arity 1) converts list/tuple/str/dict/set (and retains an existing RANGE) through sharedpy68_iterable_get_iter.next(it[, default])advances a RANGE; exhaustion raises catchableStopIteration(PY68_ERROR_STOP_ITERATION, appended afterINTERRUPT) or returnsdefault.for/ comprehensions stay onRANGE_*(noGET_ITER/FOR_ITER). Rejectiter(callable, sentinel). - Alternatives considered: New iterator heap type; rewire
forto new opcodes; exhaustion asValueErroronly. - Consequences:
for x in iter(xs):works becauseRANGE_INITaccepts RANGE.enumerate/reversedremain deferred.
- Context: Scripts and examples use CPython's
sumover lists/tuples. Python68K already exposed related aggregators (min/max/all/any/sorted) but omittedsum, which producedNameErrorat call sites. - Decision: Register import-free
sum(iterable[, start])with arity 1–2.iterablemust be a list or tuple of numbers (int/bool/float);startdefaults to0and must be numeric. Empty input returnsstart. Integer accumulation uses checked 32-bit add (OverflowError); any float promotes the running total to binary32 float with non-finite rejection, matchingOP_ADD. Strings and other containers areTypeError. - Alternatives considered: Defer until a general iterator protocol; accept
only ints (too narrow vs existing float arithmetic); support string
startfor concatenation (CPython forbids this forsum). - Consequences:
sum(range(n))works becauserangematerializes a list. Nokey=or keyword arguments.
- Context: Everyday Python
then if condition else elsewas missing.if/elseexisted only as statements; comprehensioniffilters are a separateor_testproduction. Lambda is unsupported, so the Prattorladder is the top of the expression grammar. - Decision: Parse
or_test ["if" or_test "else" expression]asPY68_AST_IF_EXP. The else-clause is a full expression, so nested ternaries associate to the right. Compile by evaluating the condition,OP_JUMP_IF_FALSEto the else branch (pops the condition), emitting the then-expr,OP_JUMPpast else, then the else-expr. No new opcode. Comprehension iterables andiffilters keep parsingor_testso[x for x in items if x]is not consumed as a ternary. Statementif/elif/whileconditions still use the full expression parser, so an unparenthesized ternary is accepted there (slightly more permissive than CPython, which usesnamedexpr_test). - Alternatives considered: A dedicated
OP_IF_EXP; left-associative else chains; allowing ternaryifto steal comprehension filters (would break Level 0.6 comprehensions). - Consequences:
a or b if c else dis(a or b) if c else d. Missingelseis a syntax error. Both branches are compiled; only one runs.
- Context:
examples/wordcount.pyneedsd[k] += 1, lexicographic ordering of strings/tuples forsorted(dict.items()), and asortedbuiltin. Plain name+=andOP_STORE_INDEXalready existed (D-0010); ordering comparisons for non-numerics raisedTypeError; D-0025 deferredsorted. - Decision: After a leading expression, accept augmented-assignment operators
when the target is
NAME,INDEX, orATTRIBUTE. Compile index targets by re-evaluating container and index aroundOP_LOAD_INDEX/ binary op /OP_STORE_INDEX(side effects run twice). Attribute targets useOP_DUP+LOAD_ATTR/STORE_ATTR. Addpy68_value_comparefor int/bool/float and same-type str/list/tuple lexicographic order; wire< <= > >=through it. Exposesorted(iterable)returning a new list (stable insertion sort; nokey/reverse). Accept list, tuple, dict keys, set values, and string characters. Restricted f-strings are provided separately (D-0043); wordcount still uses concatenation where convenient. - Alternatives considered: Rewrite wordcount to avoid
+=/sorted; implement full f-strings; addDUP_TWOinstead of re-evaluating index targets. - Consequences: Updates D-0025 to allow
sortedwithout general iterator protocol builtins. Nested-containerprintremains limited to scalar list items.
- Context: Python
istests object identity. Python68K stores None, bool, int, and float as tagged immediatePy68Values with no heap object, while str/list/tuple/dict/set/function/module/file are reference-counted heap objects. Membership recently reserved opcodes 0x1E/0x1F. - Decision: Parse
isand compoundis+notas comparison-precedence binary operators (PY68_AST_BINARY, with parser-onlyPY68_TOKEN_IS_NOT). EmitOP_IS(0x16) andOP_IS_NOT(0x17). Identity is the samePy68ValueTypeplus: None values are identical to each other; bool/int/float match when the 32-bit payload matches; heap objects match when the pointers are equal.is notis one operator, sox is not yis notx is (not y). Prefixnotstill binds less tightly than comparisons (not x is None≡not (x is None)). Chaining is left-associative like==(a is b is c≡(a is b) is c). - Alternatives considered: Heap-box all scalars so only pointer identity exists
(expensive on 68000); intern only None (would make
1 is 1false unlike the tagged model); reuseOP_EQUALwith a flag. - Consequences:
x is None/x is not Nonematch Python.True is 1is false whileTrue == 1remains true (D-0011). Equal ints/bools/floats with the same payload are identical even when CPython would not intern large ints or floats. Two equal lists, tuples, or strings are not identical unless they are the same object; eachLOAD_CONSTstring allocates a new heap string.
- Context: Language Level 0.1 assignment targets were a single name or index
store;
fortargets were a singleNAME. Everyday Pythona, b = (1, 2)was listed as unsupported. D-0015 kept tuples parenthesized because a barea, bcollides with call and assignment parsing in expression position. - Decision: Add
OP_UNPACK(0x0F,u16count). The instruction pops one list, tuple, or string and pushescountitems right-to-left so the first item is TOS; stores then run left-to-right. Wrong length isValueError(not enough values to unpack/too many values to unpack); a non-sequence isTypeError. Assignment RHS is an expression list: a comma builds a tuple, soa, b = 1, 2andx = 1, 2work. Unparenthesized name lists are assignment andfortargets only (a, b = …,for a, b in …, and parenthesized(a, b)as those targets). Comprehensionfortargets remain a singleNAME. Nested unpack ((a, b), c), starred unpack, and unpack into subscript/attribute targets stay unsupported. - Alternatives considered: Desugar to indexed loads without a new opcode
(worse errors, extra bounds checks); extend D-0015 to unparenthesized
tuples in every expression position (
return a, b), which still collides with argument lists. - Consequences: D-0015 still applies outside assignment:
return a, bremains a syntax error. Opcode 0x0F is assigned; changing it requires a bytecode-format version bump.
- Context:
PY68_TOKEN_INexisted forfor/comprehensiononly. Scripts such asexamples/wordcount.pyneed comparisonin/not inandfor char in text. - Decision: Parse
inand compoundnot+inas comparison-precedence binary operators (PY68_AST_BINARY, with parser-onlyPY68_TOKEN_NOT_IN). EmitOP_CONTAINS(0x1E) andOP_NOT_CONTAINS(0x1F). Semantics: str/str substring (empty needle is true); list/tuple equality scan; dict key presence; set membership; other containers raiseTypeError. ExtendOP_RANGE_INITiterable conversion so strings become a range over one-character strings. Adjust prefixnotso it binds less tightly than comparisons (not a in b≡not (a in b)). - Alternatives considered: Desugar
not intoCONTAINS+NOT; reject string iteration until a dedicated iterator type exists. - Consequences: Membership and string
for/comprehensions match the documented Python 3 subset. Opcode numbers 0x1E/0x1F are now assigned; changing them requires a bytecode-format version bump.
- Context: D-0032 deferred output capture pending a native request/result
contract. Users need to read a command's output into a variable (e.g.
dirorlist) instead of only seeing it on the console. - Decision: Add
py68_platform_system_captureand expose it asos.popen(command), returning captured stdout directly as astrrather than a file object. Validation matchesos.system: reject non-string, empty, or NUL-containing commands withTypeError/ValueError. The host backend redirects combined stdout/stderr to a per-process temporary file. The Amiga backend creates a uniquePIPE:name from the current task and a sequence number, launchescommand >PIPE:nameasynchronously through the user shell, then opens the reader endpoint and buffers until EOF. Because asyncSystemTagListcloses its streams, the child receives a disposableNIL:base output instead of PythonAmi'sOutput()handle; shell redirection replaces it for command stdout.SYS_Erroris not used because it is V50-only and PythonAmi supports V36+. Error output behavior therefore depends on the active shell and DOS version. The Amiga async launch cannot provide a portable child exit status, andos.popendiscards the platform return code. - Alternatives considered: A CPython-compatible
os.popenreturning a file-like object; synchronousSystemTagListredirection toT:. The temporary-file implementation failed on the target when reopening/reading the redirected process output. Returning a live file object would require process lifetime and close semantics that the platform API does not expose. - Consequences: Scripts can capture command output into a variable on both
host and Amiga when a
PIPE:handler is mounted. Amiga capture is bounded to 1 MiB including the terminator and waits for producer EOF; a child that never closes stdout can block the caller.subprocess, argument-list commands, per-childcwd/env, timeouts, andPopenremain unimplemented.
- Context: Amiga deployment needs a way to validate a script without running its side effects, while preserving the same compiler and verifier path used for normal execution.
- Decision:
--checkaccepts a command or script input, compiles and verifies it, skips builtin installation and VM execution, and returns the normal source or memory status. Valid input produces no script output; diagnostics continue to use stderr. - Alternatives considered: Parse-only validation, a separate checker, or executing in a sandbox. These alternatives would either omit bytecode verification or duplicate the production pipeline.
- Consequences: Deployment scripts can be preflighted on Amiga without executing them. Imported modules are not loaded during this top-level check; import validation remains part of normal execution.
- Context: The DOS command execution proposal requires a process backend, but output capture and asynchronous lifetime management need handles, cleanup, and child-I/O semantics that do not yet exist in the platform interface.
- Decision: Implement only
os.system(command)initially. Validate a single, non-empty string and reject embedded NUL bytes, execute synchronously with inherited standard handles, and return the native command status directly. Launch failure is mapped to an I/O runtime error. The Amiga implementation usesSystemTagList(notExecute, which only returns DOSTRUE/DOSFALSE); the host implementation uses its synchronous command primitive.subprocessandPopenremain explicitly unsupported. - Alternatives considered: Emulate
subprocesssynchronously, silently ignore capture/timeout parameters, or expose shell execution as a direct process API before the backend can enforce that distinction. - Consequences: The smallest useful API is available without claiming
shell=False, capture, timeout, environment, or process-handle semantics. A later increment must add a native request/result contract and tests for temporary-file capture before expanding the public API.
- Context: The requested time subset needs calendar fields while the language has no general user-defined object type.
- Decision: Install
time,sleep,ctime,localtime,strftime, andperf_counteras native functions.localtimereturns a dedicated reference-countedstruct_timeobject exposing the nine documentedtm_*attributes. Epoch and performance-clock values use the existing software binary32 value representation; calendar conversion and formatting use the platform C time services. - Alternatives considered: Return an unnamed tuple, add a general attribute dictionary, or expose only formatted strings.
- Consequences:
localtime().tm_yearandstrftime(format, localtime())are supported without expanding the user object model. Host monotonic timing is backed by the host clock service and Amiga timing byDateStamp; sub-second precision follows each platform's available clock resolution.
- Context: Seeding the random example with
int(time())repeats whenever two processes start in the same epoch second. Scaling the epoch float by 1000 would exceed the signed 32-bit language integer range. - Decision: Expose
time_tick()as a signed 31-bit millisecond value. Host implementations use the host elapsed/system clock and Amiga uses the millisecond value derived from DOSDateStamp(), masked to0x7fffffff. - Alternatives considered: Keep second-resolution seeds, use a large integer timestamp, or add a platform-specific random source.
- Consequences: Short-lived examples receive varying seeds without requiring 64-bit integers or floating-point conversion. The tick wraps periodically, so it is suitable for seeding and not a persistent timestamp.
- Context: Authors want vbcc/vasm performance helpers callable from pythonami without rebuilding the interpreter. Classic AmigaOS
.library(Resident/LibInit/LVOs) is heavy for this use case; hostdlopenis out of scope. - Decision: Amiga-only builtin
load_library(path)usesLoadSegon a relocatable Hunk file (*.py68k). First hunk payload after the seglist next-pointer is aPy68ExtHeader('PY68', ABI 1, export table). Each export becomes aPy68NativeFunctionon a returned module.UnLoadSegruns when the module is destroyed (after clearing globals). Public ABI isinclude/py68k_ext.h. Plugins must not linkstartup.o/vc.lib/ NDKamiga.lib. - Alternatives considered: Real AmigaOS
.libraryviaOpenLibrary; extendingimportto auto-load natives; host ELFdlopen. - Consequences: Scripts keep the library module alive while calling exports; escaped native refs after unload are undefined.
importremains.py-only. Sample + vasm workflow live underext/demo_add/andmake amiga-ext.
- Context:
examples/test_random.py(import random, which itself doesimport randgen) ran to completion with correct printed output on Amiga, then the process crashed with a Guru Meditation after the script finished. Nothing inMakefile.amigaorsrc/main.cever requested a process stack size, so the binary inherited whatever stack the launching Shell/Workbench icon provided (often as little as 4 KiB). Recursive-descent tokenizing, parsing, and compiling, plus a nestedimportre-enteringpy68_vm_execute_moduleon the C call stack (once per imported module, stacked on top of the outer module's own still-active frame), can exceed a small default stack; the resulting corruption of adjacent memory only faults later, when the corrupted C stack frames unwind during cleanup — after the script's own output has already been written. - Decision: Define
long __stack = 65536L;insrc/main.cunder#ifdef PY68K_AMIGA. vbcc's+aos68klib/startup.orecognizes this SAS/C-style global and allocates a process stack of that size instead of inheriting the caller's, independent of any CLIStackoverride behavior. Host builds are unaffected (PY68K_AMIGAis only defined for the Amiga build). - Alternatives considered: Convert nested
importexecution to an explicit worklist instead of C recursion (larger refactor, deferred); require callers to raise the CLIStackbefore runningpythonami(undiscoverable, not enforceable from Workbench). - Consequences:
examples/test_random.pyand other multi-levelimportchains need real Amiga/emulator re-verification with a freshly rebuilt binary.__stacksize may need future tuning if deeper import chains or recursion are added.
- Context: The CLI needs deterministic execution statistics without changing normal script output or exit status.
- Decision:
--debugis accepted before-cor a script path and emits a delimited report through the platform stderr abstraction after VM execution and before source/code cleanup. Source file count, source bytes/lines, tokens, and bytecode metrics describe only the top-level source unit in this increment. - Alternatives considered: Always-on diagnostics, reporting after cleanup, or aggregating imported modules before the import metrics contract is defined.
- Consequences: Report writes are best-effort and cannot replace the original status.
-Vand--helpremain report-free, including when preceded by--debug; imported-module aggregation remains future work.
- Context: NDK 3.2 documents
ErrorOutput()as V47-only. Calling that LVO on Kickstart 2.x–3.1 crashes after successful script output when--debugor diagnostics first touch stderr. - Decision:
py68_platform_write_stderron Amiga usesErrorOutput()only whenDOSBase->dl_lib.lib_Version >= 47; otherwise it writes topr_CESwhen non-zero and falls back toOutput(). Reject a null file handle beforeWrite(). - Alternatives considered: Require AmigaOS 3.2, always write diagnostics to
Output(), or open a fixed console. - Consequences: Separated stdout/stderr redirection works on OS 3.2 shells; on older systems stderr merges with stdout unless the process already has
pr_CESset.
- Context: Linux is the primary development environment, while this workspace also needs a local compiler for host tests on Windows.
- Decision: Keep
gccas the default host compiler and support Clang through theHOST_CCmake variable. The Windows workspace uses LLVM-MinGW Clang 22.1.8 installed locally through WinGet. Visual Studio 2026 is installed, butcl.exeis not the configured compiler and must be evaluated from a Developer PowerShell if support is added later. - Alternatives considered: Make the repository depend on Visual Studio, add a committed compiler binary, or change the Linux default to Clang.
- Consequences: Linux builds remain unchanged with
make test; Windows host tests can useHOST_CC=<path-to-clang.exe>. The compiler installation is a machine prerequisite, not a repository dependency, and Amiga builds remain controlled byMakefile.amiga.
- Context: The brief requires
signed longforPy68I32, while modern 64-bit hosts commonly definelongas 64 bits. - Decision: Use
signed intandunsigned intfor host builds andsigned longandunsigned longfor Amiga builds, with compile-time four-byte assertions in both configurations. - Alternatives considered: Force the host compiler into an LLP32 data model, or reject common 64-bit host compilers.
- Consequences: The language width is explicit and portable on the supported host and Amiga targets; serialized formats must continue to use
Py68U8byte encoding rather than C type layout.
- Context: Python68K must be checked both on the Motorola 68000 target and on a current Linux Intel host without confusing compilation evidence with execution evidence.
- Decision: Use
vbccm68kfor 68000 compiler/object checks andvbcci386or GCC for Intel-host compatibility checks. Report emulator and hardware execution separately from compiler and host results. - Alternatives considered: Treat the Amiga Hunk build as sufficient, or rely only on GCC and desktop tests.
- Consequences: Host sanitizers remain fast and authoritative for portable-core defects; target, emulator, and hardware results are reported separately, and no compatibility claim is made without actual execution evidence.
- Context: The sibling assembler project contains
wbstartup.s, while the Python68K Amiga build uses vbcc's+aos68kC runtime. - Decision: Keep vbcc
startup.oas the sole Workbench startup/exit owner. Do not link or call the siblingwbstartup.sfrom C code. - Alternatives considered: Add
WBStartup/WBExitcalls aroundmain, or replace vbcc startup with a custom assembly entry. - Consequences: The current build follows the vbcc/NDK Workbench handshake contract; a second message receive/reply must not be introduced. Workbench retesting must use a freshly rebuilt NDK-linked binary.
- Context: The NDK
amiga.liband vbcc target libraries are separate ABI/runtime families. - Decision: Link the Python68K Amiga C program through the vbcc
+aos68kconfiguration and its targetstartup.o/vc.lib; use the NDK only as an optional reference/source of headers, not as a library mixed into this link. - Alternatives considered: Append NDK
lib/amiga.libto the vbcc link command. - Consequences: The C runtime, Workbench startup handshake, DOS inline calls, and vbcc ABI remain coherent. Amiga runtime execution must be retested with freshly rebuilt artifacts.
- Context: The debug build succeeds while the release binary crashes in Amiga startup or early execution, a classic sign of a 68000 optimization or delayed-pop issue rather than a source-logic error.
- Decision: Keep
-use-framepointerand-no-delayed-poppingin both debug and release builds to preserve a stable stack frame and avoid release-only Guru Meditation behavior. - Alternatives considered: Keep the release build at
-O=2alone, or add a custom assembly startup wrapper. - Consequences: The release artifact follows the same stable ABI assumptions as the debug build; any remaining emulator or hardware crash will be treated as a true runtime issue, not a compiler flag mismatch.
- Context:
Py68GlobalEntryand the builtin table originally stored aname_indexinto a code object's constant/name table. EachPy68Code(module or function body) owns its own independently-numbered name table, so the same numeric index can denote different identifiers in different code objects. Once function bodies could read/write module globals and call builtins, this caused genuine cross-code-object name collisions (a function's local index 2 could collide with the module's index 2 for an unrelated name). - Decision: Store
(const Py68U8 *name, Py68U16 name_length)in bothPy68GlobalEntryand the builtin registry, and resolve by byte-content comparison instead of index equality.py68_global_set_copy/get_copyandpy68_builtin_set_copy/get_copytake name bytes and length directly. - Alternatives considered: Give every code object a shared/global name table (larger refactor, touches the compiler's per-function name emission); intern all names into one process-wide table with stable indices.
- Consequences: Global/builtin lookup is a linear byte comparison rather than an index compare, which is acceptable at Language Level 0.1's expected program sizes on a 68000. A future increment may revisit interning with FNV-1a hashing (already implemented for
Py68String) if lookup cost becomes a concern for larger programs.
- Context: Python raises
UnboundLocalErrorwhen a function reads a local variable before any assignment reaches it on the executed path (e.g.if False: x = 1thenreturn x). The frame previously initialized every local slot toNone, silently masking this class of bug and diverging from Python's documented semantics. - Decision: Add
PY68_VALUE_UNBOUNDas a distinctPy68ValueTypeand initialize every non-parameter local slot to it when a frame is set up.OP_LOAD_LOCALchecks for this sentinel and raises a runtime error instead of returning it as a usable value;OP_STORE_LOCALoverwrites it normally. - Alternatives considered: Track "assigned" state via a separate bitmask per frame; perform a static "definitely assigned" data-flow analysis at compile time.
- Context: Python
and/ormust not evaluate the right-hand operand when the left-hand value already decides the result, and must return the deciding operand rather than a coerced boolean. - Decision: Compile
and/orwithOP_JUMP_IF_FALSE_OR_POP/OP_JUMP_IF_TRUE_OR_POP. Verifier treats the jump path as keeping TOS and the fall-through path as popping TOS (stack effect -1). - Alternatives considered: Always evaluate both sides into booleans with
OP_AND/OP_ORopcodes. - Consequences: Matches Python value-preserving short-circuit semantics; empty strings/lists are falsy via updated truthiness rules.
- Context:
forcompilation leaves a range object on the value stack forOP_RANGE_NEXT. Exhausted iteration pops it before joining the exit/else path, butbreakpreviously jumped to the same join with the iterator still on the stack, so the verifier reported inconsistent stack depth. - Decision: Mark for-loop contexts with
pop_on_breakand emitOP_POPimmediately before each for-breakjump.OP_POPreleases the popped value. While-loops leavepop_on_breakclear. - Alternatives considered: Dedicated break-cleanup label after the loop, or changing
OP_RANGE_NEXTmetadata so break could jump through a shared pop block only. - Consequences: for-
breakverifies and runs; else clauses remain skipped on break; continue is unchanged (jumps back toRANGE_NEXTwith the iterator still under the body).
- Context: Language Level 0.1 documents list item assignment and
OP_STORE_INDEXexisted, but the statement parser only accepted bare-name targets, soL[1] = 99failed before codegen. - Decision: After parsing a leading expression, if
=follows and the expression is anINDEXnode, emit an assignment whosetargetis that index. Compile as container, index, value, thenOP_STORE_INDEX. Symbol analysis treats the target as a use (not a new binding). String item assignment remains a TypeError. - Alternatives considered: Restrict targets to
NAME[index]only, or invent a separate AST kind. - Consequences: Nested stores such as
G[1][1] = 40work because the outer index container may itself be an index expression.
- Context: Comparisons required both operands to be
INT, soNone == NoneandTrue == 1raised TypeError despite Language Level 0.1 scalar equality rules. - Decision: Handle
OP_EQUAL/OP_NOT_EQUALforNonefirst (Noneequals onlyNone). AcceptBOOLalongsideINTfor equality and ordering by using the stored 0/1 integer payload (Python numeric policy for booleans). - Alternatives considered: Coerce bool to int at load time only, or reject bool/int mixed comparisons.
- Consequences:
True == 1,False == 0, andNone == Nonematch Python; unrelated types still TypeError on arithmetic/order paths that do not special-case them.
- Context: Phase 6 requires AmigaDOS file and environment access. Language Level 0.1 has no attribute access or
with, so Python-style file objects with methods are unavailable. Amiga “environment” for this project means DOS assigns, notENV:GetVar. - Decision: Expose function builtins
fopen/fclose/fread/freadline/fwrite/exists/remove/renamewith modesr/w/a/rb/wb/ab. Binary and text both use string payloads (nobytestype). Host installsgetenv/setenv/unsetenv; Amiga installsassign_get/assign_add/assign_removeon the same platform_var_* layer (AssignPath,AssignLock(name,0),Lock("name:")+NameFromLock). Platform I/O stays infile_host.c/file_amiga.c;PY68_OBJECT_FILEcloses on final release.fread(handle, count)allocatesmin(count, remaining)via Seek/ftell, notcountbytes upfront, so large “read all” counts are Amiga-safe. Relativefopenpaths use the process current directory. - Alternatives considered: method-style
open(), AmigaGetVar/SetVar, full CPython mode matrix (+,x); pre-allocatecount+1for everyfread. - Consequences: Scripts targeting Amiga should call
assign_*. Host tests exercise file APIs and POSIX env. Emulator/hardware assign behavior remains owner-verified. Non-seekable handles are unsupported for sizedfread.
- Context: Language Level 0.1 tokenized both
/and//as floor-divide. Level 0.3 adds binary32 float and Python-3 true division. - Decision:
/emitsOP_TRUE_DIVIDEand always yields a finitefloat.//remains integer floor division. NaN and Inf results are rejected asValueError. No 68881 and no Amiga IEEE library: binary32 add/mul/div/parse/print are integer-only software insrc/float.c(-cpu=68000 -fpu=0). - Alternatives considered: Keep
/as floor until a later level; use binary64. - Consequences: Existing scripts that used
/for floor-int must switch to//. Decision is a documented language-level break from 0.2.0.
- Context: Level 0.1 had no attribute access; file I/O used function builtins. Dict methods,
with, and imports needobj.name. - Decision:
obj.namecompiles toOP_LOAD_ATTR. Each heap type has a static method table. Lookup builds aPY68_OBJECT_BOUND_METHOD{self, native}consumed byOP_CALL. Modules resolve attributes in their global table.STORE_ATTRis allowed only on module objects. No user-defined attributes or classes. - Alternatives considered: Function-style
dict_getonly; full instance dictionaries. - Consequences:
list.appendexists alongsidelist_append.sys.path.appendworks becausesys.pathis a list.
- Context:
()already grouped expressions. Barea, bwould collide with call and assignment parsing. - Decision: Accept only parenthesized tuples:
(),(a,),(a, b). A single(expr)remains grouping. - Alternatives considered: Full Python tuple display including unparenthesized targets.
- Consequences:
return a, bis a syntax error; writereturn (a, b). Assignment andfortargets later gained unparenthesized name lists without changing this expression-display rule (D-0038).
- Context: Dict and set need a value hash/equality protocol. Level 0.1 already rejects cyclic lists.
- Decision: Hashable:
None,bool,int,str, and tuples of hashable items. Unhashable keys raiseTypeError. Inserting a value that would make a dict reachable from itself isValueError: cyclic containers are not supported.OP_EQUALuses the same equality helper (so strings and lists compare). - Alternatives considered: Allow all objects as keys via identity; add a tracing GC instead of cycle rejection.
- Consequences: Deterministic FNV-1a (strings) plus identity-free scalar hashes; no randomized hashing.
- Context: Runtime errors were a single aborting
Py68Error. Level 0.4 needstry/except/finally/raise. - Decision: Token/syntax/bytecode/memory/internal errors stay uncatchable. Other
Py68ErrorKindvalues becomePY68_OBJECT_EXCEPTIONobjects.OP_SETUP_TRY/OP_POP_TRYrecord handler IP and stack depth per frame. Matching compares exception kind to a builtin exception-type native (TypeError, …), not a class MRO.finallybodies are compiled inline beforereturn/break/continue. - Alternatives considered: Full exception class hierarchy; CPython block stack with Why flags.
- Consequences:
except TypeError as eworks; user-defined exception types do not.
- Context: File I/O had no context managers. Level 0.4 adds
with. - Decision: Compile
with EXPR as NAMEto keep the manager on the stack, call__enter__, bind the result, wrap the body inSETUP_TRY, and call__exit__(None, None, None)on both success and handler paths. First context manager:PY68_OBJECT_FILE(__enter__returns self,__exit__closes). - Alternatives considered: Method-style
open(); noastarget only. - Consequences:
with fopen(path, mode) as f:is the supported form.
- Context: Level 0.1 had no import system. “Module” meant the top-level script code object.
- Decision:
import/from/ascompile toOP_IMPORT_NAME/OP_IMPORT_FROM. The loader readsname.pyfrom the importing source directory, thensys.pathentries. Compiled modules arePY68_OBJECT_MODULEobjects cached by resolved path. Relative imports andimport *stay unsupported.sysis a builtin module (path,modules,argv). - Alternatives considered: Full package/
__init__.pytrees; CPython.pyc. - Consequences: Multi-file programs work for sibling
.pyfiles; no CPython bytecode compatibility.
- Context: Imported modules must execute once, failed imports must not leave stale globals, and recursive imports must not recurse indefinitely.
- Decision: Insert a module in the cache with a private loading flag before
executing it. A lookup of a loading module reports
ImportError: import cycle detected; successful execution clears the flag, while any failure removes the cache entry and releases the partial module. - Alternatives considered: Execute imports without caching, expose partially initialized modules to cycles, or add package-style import state.
- Consequences: Cache identity and one-time execution are deterministic. Cycles are rejected intentionally; packages, dotted names, and relative imports remain outside this increment.
- Context: A Python
randomlibrary needsimport randomand callable defs that read module-level PRNG state. After import, destroying the module code object left function objects with dangling bytecode; nestedimportclearedexecuting_module, so defs after an import bound the wrong globals. - Decision: Retain each module's
Py68Codeon the module (owned_code) for the module lifetime.MAKE_FUNCTIONrecords the defining module;LOAD_GLOBALin that function uses the module's globals. Nested imports save/restoreexecuting_module. - Alternatives considered: C-only PRNG builtins; require
from module import *style flattening; retain code in each function object. - Consequences: Imported user functions remain callable and see their defining
module globals. Module teardown releases globals before destroying
owned_code.
- Context: The 0.1 tokenizer classified
setanddictas unsupported keywords, unlike Python where they are builtins. - Decision: Remove them from the keyword table so they tokenize as
PY68_TOKEN_NAMEand resolve to constructor builtins. - Alternatives considered: Keep them as keywords that introduce literal syntax only.
- Consequences:
set = 1is a legal (if unwise) assignment that shadows the builtin.
- Context: The string builtins reference mirrors CPython Unicode
strAPIs. Python68K strings are 8-bit byte strings (Language Level contract), not Unicode. - Decision: Implement Phase-2/Phase-3
strmethods and related text builtins with ASCII / 8-bit semantics only. Case mapping and classifiers operate onA-Z/a-zand ASCII digit/whitespace/printable ranges; other bytes are left unchanged (case) or rejected by classifiers as appropriate.casefoldis an ASCII alias oflower.isdecimal/isdigit/isnumericall mean ASCII'0'-'9'.isidentifierfollows the tokenizer’s ASCII name rules.chraccepts0..255only (not0..0x10FFFF). Literal escapes\\ \' \" \n \r \t \xHHare decoded when materializing string constants. - Alternatives considered: Fake Unicode tables; leave escapes undecoded in constants.
- Consequences: Scripts that rely on Unicode casefolding, numeric characters, or code points above 255 are out of scope. Documented divergence from CPython is intentional.
- Context: CPython exposes
str.maketransas a static method on thestrtype object. Python68K has no type objects. Fullstr.format/format_map,bytes/bytearray/encode, andeval/exec/compileconflict with Level 0.1 constraints (no kwargs, no bytes type, security). - Decision: Expose
maketrans(x[, y[, z]])as an import-free builtin returning adictof int→int/None mappings;str.translate(table)consumes that dict (or any compatible dict). Provide minimalformat(value[, format_spec])for ints ('',d, width,0-pad such as04d). Deferbytes/bytearray/encode,eval/exec/compile, fullstr.format/format_mapwith replacement fields and kwargs. General iterator builtins beyonditer/next(enumerate/reversed,iter(callable, sentinel)) remain deferred (D-0042). Restricted f-strings are provided in D-0043.sorted(iterable)is provided withoutkey/reverse(D-0039).asciiescapes bytes>= 128as\xHH;reprleaves high bytes literal when printable. - Alternatives considered: Opaque translation-table object; alias
asciitorepr. - Consequences:
maketransis a name in the builtin table, notstr.maketrans. Advanced formatting and encoding remain future work.sumis provided separately (D-0041).
- Context: Python 3 list/set/dict comprehensions run in a nested function scope so loop targets do not leak. Python68K does not implement nested
def, closures, or cell variables. - Decision: Compile
[elt for x in it if cond],{elt for ...}, and{k: v for ...}inline in the current code object usingBUILD_LIST/BUILD_SET/BUILD_DICTplus the existingRANGE_INIT/RANGE_NEXTloop, thenLIST_APPEND/SET_ADD/MAP_ADD. The targetxis a normalforassignment: a function local if the comprehension appears in a function, otherwise a module global. Nestedforclauses and zero or moreiffilters per clause are supported. Generator expressions(elt for ...)are a targeted syntax error. - Alternatives considered: Desugar to an anonymous nested function (requires closures); emit only list comprehensions and reject set/dict forms; keep the comprehension result in a compiler-generated temp name.
- Consequences:
xs = [n for n in range(3)]; print(n)prints2, matchingfor. A comprehension target assigned anywhere in a function makes that name local throughout the function (unbound reads raiseNameError). Empty{}remains an empty dict;{x for x in it}is a set comprehension and{k: v for ...}is a dict comprehension, so they do not collide with{k: v}/{a, b}literals (D-0020).
- Context: A CPU-bound Python68K loop cannot be aborted, because the VM never inspects the task signal set. AmigaOS is preemptively multitasking at the Exec level (Workbench is only the GUI launcher, not a scheduler), so a busy loop does not starve other programs and there is no Windows-style message pump that must be called for fairness.
- Decision: Add
py68_platform_poll(runtime, flags)withPY68_POLL_BREAK(consume a pending user break) andPY68_POLL_YIELD(politeness hint only). The Amiga implementation usesCheckSignal(SIGBREAKF_CTRL_C)and, for the yield hint,Forbid(); Permit();because Exec has noYield(). The host implementation uses aSIGINThandler and avolatile sig_atomic_tflag.py68_platform_signal_breakposts a break to the current process (Signal(FindTask(NULL), SIGBREAKF_CTRL_C)on Amiga) and makes the behaviour testable on the host. The VM calls the hook only on taken backward branches, throttled byruntime->poll_interval(defaultPY68_POLL_INTERVAL_DEFAULT, 256);poll_interval == 0disables polling. A consumed break raisesPY68_ERROR_INTERRUPT("KeyboardInterrupt"), which is deliberately absent frompy68_error_is_catchable, soexcept:cannot swallow it.mainmaps it to exit code 10 (AmigaDOSRETURN_ERROR). - Alternatives considered: Poll every instruction (unaffordable dispatch cost on 68000); poll on call/return as well (recursion is already bounded by the recursion limit);
Delay(1)as the yield primitive (costs a full 20 ms tick, so it is reserved for an explicit script-level yield); makingKeyboardInterruptcatchable like CPython (would let a bareexceptinside a loop defeat Ctrl-C). - Consequences:
PY68_ERROR_INTERRUPTis appended last inPy68ErrorKindso existing kind numbers stay stable. Straight-line code pays nothing; a loop iteration pays one decrement and branch. Scripts cannot catch or suppress a user break at Language Level 0.5. Script-visibleyield_cpu/set_priority/check_breakand Ctrl-C verification under emulation remain future increments.
- Context: D-0034 added the VM-level break poll. Scripts still need a way to observe a break themselves, to tune the poll rate, and to be explicitly polite to other tasks.
- Decision: Register
check_break(),yield_cpu(),set_poll_interval(count), andget_poll_interval()as import-free builtins in the common table, alongsidetime/sleep, rather than behind anamigaorsysmodule.check_break()consumes the pending break and returnsTrueexactly once, so a script that calls it takes responsibility for stopping.yield_cpu()passes onlyPY68_POLL_YIELDand never consumes a break.set_poll_intervalaccepts a non-negative int (0disables VM polling), rejects other types withTypeErrorand negatives withValueError, and resetspoll_counterso the new interval applies immediately. - Alternatives considered: A
sysmodule namespace (Python68K has no attribute-settable module objects for runtime knobs andsysis already a fixed module); makingcheck_break()non-consuming (a loop would then seeTrueforever and the VM poll would raise anyway); mappingset_poll_intervalonto a CPython-stylesys.setcheckintervalname (misleading, since Python68K counts backward branches, not instructions). - Consequences: Four more names occupy the builtin table on every target, including the host, so host and Amiga scripts stay source compatible.
yield_cpu()is a no-op on the host. Task priority control (SetTaskPri) is still not exposed.