This release is about correctness: fixing builtins that looked implemented but produced wrong results at runtime, unifying DRAW_CIRCLE to 16 segments across both compilers, and making MOVE actually work. It also ships 11 ready-to-open snippet projects as learning material.
MOVE now works in both compilers
MOVE(x, y) sets a coordinate origin so all subsequent DRAW_LINE calls are relative to that position. It was broken in two different ways.
Core (vectrexc): MOVE was simply not in the builtins list. When the compiler saw MOVE(...), it fell through to user-function handling and emitted JSR MOVE — which failed at link time with "undefined symbol".
Buildtools (vpy_cli): MOVE was in the list, but was calling Moveto_d_7F without the required BIOS setup (DP=$D0). And DRAW_LINE_WRAPPER always calls Reset0Ref first anyway, which resets the beam to center and cancels any prior Moveto_d call.
The fix (both compilers): MOVE(x, y) now stores the coordinates into two 1-byte signed RAM variables VPY_MOVE_X and VPY_MOVE_Y, initialised to 0 at startup. DRAW_LINE_WRAPPER adds them via ADDA/ADDB to the start-point before the Moveto_d call.
def loop():
MOVE(-60, 60) # shift origin to top-left area
DRAW_LINE(0, 0, 0, -40, 127) # draws relative to (-60, 60)
DRAW_LINE(0, -40, 30, -40, 127)DRAW_CIRCLE upgraded to 16 segments everywhere
The runtime path (radius is a variable) was drawing an 8-segment octagon. The constant path (radius is a literal) was already using a 16-segment polygon. This made circles look noticeably different depending on how you called them.
Both paths now use a 16-sided polygon. The runtime path computes the 16 vertex offsets using the newly-added MUL instruction with 4 precomputed fractions:
| Fraction | Multiplier | Approx |
|---|---|---|
| cos(π/8) · r | #0x98 >> 8 | 0.5879r |
| cos(3π/8) · r | #0x63 >> 8 | 0.3873r |
| sin(π/8) · r | #0x63 >> 8 | 0.3873r |
| sin(3π/8) · r | #0x98 >> 8 | 0.5879r |
This required adding MUL (opcode 0x3D) to both assemblers — it was absent from both.
Math builtins fixed in core
abs() was the only math function that actually worked. min(), max(), and clamp() all compiled but produced wrong results at runtime. There were four layered bugs:
Bug 1 — Semantic gate. BUILTIN_ARITIES (the list the validator checks) only contained ABS. Calling min(30, 70) immediately produced "Unknown function: min" before code generation even started.
Bug 2 — RAM not allocated. analyze_runtime_usage() triggers allocation of temp RAM slots like TMPLEFT/TMPRIGHT. It never scanned for MIN/MAX/CLAMP calls. The assembler then failed with "undefined symbol: TMPLEFT".
Bug 3 — Wrong comparison instruction. MIN was using SUBD TMPRIGHT; BGT — but SUBD modifies the D register. After the subtraction, D no longer holds the first argument. The branch target tried to store the corrupted D as the result. Fixed by replacing with CMPD RESULT; BLE, which is non-destructive.
Bug 4 — Assembler drops inline label instructions. Generated ASM had lines like MIN_FIRST_1: STD RESULT (label and instruction on the same line). The assembler's parse_and_emit_instruction() receives the full raw line, splits on whitespace, and gets mnemonic = "MIN_FIRST_1:" — no match in the dispatch table, instruction silently dropped. Fixed by emitting the label on its own line followed by the instruction.
After all four fixes: min(30, 70) = 30, max(30, 70) = 70, clamp(150, 0, 100) = 100. ✓
11 snippet projects
examples/individual_tests/ now contains 11 self-contained .vpyproj projects openable directly in Vectrex Studio. Each tests one feature:
| Project | Tests |
|---|---|
draw_circle/ | DRAW_CIRCLE with various radii |
draw_line/ | DRAW_LINE basics |
draw_move/ | MOVE + DRAW_LINE offset |
draw_rect/ | DRAW_RECT bounding boxes |
draw_vector/ | DRAW_VECTOR asset rendering |
joystick_buttons/ | J1_BUTTON_1–4 with debounce |
joystick_position/ | J1_X/J1_Y analog and digital |
math_functions/ | abs, min, max, clamp |
play_music/ | PLAY_MUSIC / STOP_MUSIC |
print_number/ | PRINT_NUMBER signed decimal |
print_text/ | PRINT_TEXT positioning |
Open any project, hit Build, and it runs in the emulator immediately. Useful as copy/paste starting points.
Bug inventory
| # | Component | Symptom | Root cause |
|---|---|---|---|
| 1 | core: MOVE | undefined symbol: MOVE | Not in builtins list |
| 2 | buildtools: MOVE | No visible effect | Moveto_d called without BIOS setup; Reset0Ref cancels it |
| 3 | both: DRAW_CIRCLE runtime | Octagon instead of smooth circle | 8-seg instead of 16-seg polygon |
| 4 | both: assembler | undefined symbol: MUL | MUL opcode 0x3D missing from both assemblers |
| 5 | core: min/max/clamp | Unknown function error | Not in BUILTIN_ARITIES |
| 6 | core: min/max/clamp | undefined symbol: TMPLEFT | analyze_runtime_usage() didn't scan for them |
| 7 | core: min/max | Wrong result (min returns larger) | SUBD mutates D; fixed with CMPD |
| 8 | core: abs/min/max/clamp | Instruction after label silently dropped | Assembler parsed "LABEL:" as mnemonic and discarded the instruction |
Commits
997b91be fix: ABS/MIN/MAX/CLAMP label+instruction on same line drops instruction
9ce715bc fix: MIN/MAX used SUBD+BGT giving wrong sign comparison vs CMPD
ae946c06 fix: MIN/MAX/CLAMP not allocating TMPLEFT/TMPRIGHT in RAM
137c47be fix: add MIN, MAX, CLAMP, MUL_A, DIV_A, MOD_A to BUILTIN_ARITIES
7320bf77 fix: implement MOVE builtin in both compilers
ec18bef5 feat: add MUL instruction (opcode 0x3D) to both assemblers
09414ff0 fix: DRAW_CIRCLE runtime upgraded from 8-seg octagon to 16-seg polygon
eff91ade buildtools: DRAW_CIRCLE with constants uses inline 16-gon path
14ebe424 DRAW_CIRCLE: standardize to 16 segments in both compilers