All posts

v0.1.1 — Math builtins, MOVE, and 16-segment circles

Eight runtime bugs fixed: math builtins that compiled but gave wrong answers, DRAW_CIRCLE differences between constant and variable paths, and MOVE doing nothing. Plus 11 ready-to-run snippet projects.

releasevectrexvpybugfix

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:

FractionMultiplierApprox
cos(π/8) · r#0x98 >> 80.5879r
cos(3π/8) · r#0x63 >> 80.3873r
sin(π/8) · r#0x63 >> 80.3873r
sin(3π/8) · r#0x98 >> 80.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:

ProjectTests
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

#ComponentSymptomRoot cause
1core: MOVEundefined symbol: MOVENot in builtins list
2buildtools: MOVENo visible effectMoveto_d called without BIOS setup; Reset0Ref cancels it
3both: DRAW_CIRCLE runtimeOctagon instead of smooth circle8-seg instead of 16-seg polygon
4both: assemblerundefined symbol: MULMUL opcode 0x3D missing from both assemblers
5core: min/max/clampUnknown function errorNot in BUILTIN_ARITIES
6core: min/max/clampundefined symbol: TMPLEFTanalyze_runtime_usage() didn't scan for them
7core: min/maxWrong result (min returns larger)SUBD mutates D; fixed with CMPD
8core: abs/min/max/clampInstruction after label silently droppedAssembler 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