Skip to content

Commit 5f717fd

Browse files
committed
rgc2ugb + rgc-lint: numeric line-label preservation + portable hardening
Tokenizer captures leading line numbers into Statement.line_num instead of stripping. Transpiler scans GOTO/GOSUB/THEN/RESTORE/ ON-GOTO refs and preserves only referenced numeric labels on output. Mixed numbered/unnumbered code is an error (E002 in linter, hard error in transpiler) — picks up half-converted classic-BASIC sources before they reach ugBASIC. Also bundles accumulated portable-subset work since last MVP commit: - SIGNED WORD intermediate-typing in trig_lut.rbas (BYTE overflow on X*18 + AMP*ISIN_RESULT was clustering the demo wave on retro targets). - Sine LUT bumped ×100 → ×1000 for sprite-motion precision. - sine_lut_demo.rbas + trig_lut.rbas added under examples/portable/ as the canonical portable trig pattern (GOSUB-based to span ugBASIC PROCEDURE / rgc-basic FUNCTION dialects). - EMPTY/TILE added to KEEP_UPPERCASE so Python output matches TS port byte-for-byte. - W003 (numeric-GOTO target) now suppressed when source is fully numbered — old form false-flagged classic numbered programs. Snapshot tests: - new tests/portable/numbered_lines.bas + locked snapshot - new tests/non-portable/mixed_line_numbers.bas - existing snapshots refreshed for EMPTY TILE = 32 auto-emit
1 parent 3aea25a commit 5f717fd

19 files changed

Lines changed: 830 additions & 43 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
' ============================================================
2+
' sine_lut_demo.rbas — portable sine wave via LUT
3+
'
4+
' Identical output across rgc-basic native + every ugBASIC
5+
' retro target (C64, VIC-20, Atari, CPC, MSX, ZX Spectrum,
6+
' CoCo, Plus/4 …). No SIN() call — pure integer math.
7+
'
8+
' Pattern: read sine value from precomputed table via GOSUB,
9+
' scale by amplitude, offset to centre vertically, plot.
10+
'
11+
' Why GOSUB and not FUNCTION? ugBASIC uses PROCEDURE / rgc-
12+
' basic uses FUNCTION; GOSUB works in both. See trig_lut.rbas
13+
' for the full discussion.
14+
' ============================================================
15+
16+
#INCLUDE "trig_lut.rbas"
17+
18+
' DIM as SIGNED WORD so intermediate math doesn't overflow on
19+
' ugBASIC's BYTE-default arithmetic. `AMP * ISIN_RESULT` reaches
20+
' 9 * 1000 = 9000 (fits WORD, blows BYTE). rgc-basic native uses
21+
' float; declaring as WORD keeps both dialects honest.
22+
DIM X AS SIGNED WORD
23+
DIM Y AS SIGNED WORD
24+
DIM AMP AS SIGNED WORD
25+
DIM CENTRE AS SIGNED WORD
26+
27+
CLS
28+
29+
GOSUB init_trig_lut
30+
31+
' Plot a 2-cycle sine wave across 40 columns, centred on row 10.
32+
' AMP = 9 (±9 row swing)
33+
' CENTRE = 10 (vertical centre)
34+
' 2 cycles over 40 cols = 720 degrees / 40 = 18 deg per X
35+
' so DEG = X * 18.
36+
AMP = 9
37+
CENTRE = 10
38+
FOR X = 0 TO 39
39+
ISIN_DEG = X * 18
40+
GOSUB isin
41+
Y = CENTRE + (AMP * ISIN_RESULT) / 1000
42+
TEXTAT X, Y, "O"
43+
NEXT X
44+
END

examples/portable/trig_lut.rbas

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
' ============================================================
2+
' trig_lut.rbas — portable trigonometry via lookup tables
3+
'
4+
' Drop-in replacement for SIN/COS in code that targets the
5+
' rgc-basic-portable subset (ugBASIC retro targets + native
6+
' rgc-basic). Pure-integer math via 360-entry sine LUT.
7+
' Identical results across every target.
8+
'
9+
' Calling convention: GOSUB-based to stay portable. ugBASIC
10+
' prefers PROCEDURE / rgc-basic uses FUNCTION; GOSUB works
11+
' everywhere with no translation needed.
12+
'
13+
' Usage:
14+
' #INCLUDE "trig_lut.rbas"
15+
' GOSUB init_trig_lut
16+
'
17+
' ' integer sine — set ISIN_DEG, GOSUB, read ISIN_RESULT
18+
' ISIN_DEG = 45
19+
' GOSUB isin
20+
' PRINT ISIN_RESULT ' = 707 (i.e. 0.707)
21+
'
22+
' ' integer cosine — set ICOS_DEG, GOSUB, read ICOS_RESULT
23+
' ICOS_DEG = 90
24+
' GOSUB icos
25+
' PRINT ICOS_RESULT ' = 0
26+
'
27+
' Sprite motion idiom — smooth weaving at frame rate:
28+
' ISIN_DEG = FRAME_COUNTER * 6 ' 6° per frame, 60° per sec @ 60 fps
29+
' GOSUB isin
30+
' SPR_Y = BASE_Y + (AMP * ISIN_RESULT) / 1000
31+
' SPRITE my_sprite AT SPR_X, SPR_Y
32+
'
33+
' Both routines return value SCALED * 1000 — so a result of 870
34+
' means 0.870 in real terms. The ×1000 scale gives 10× the
35+
' precision of a ×100 LUT, dramatically reducing visible
36+
' clustering at peaks/troughs when scaling to small amplitudes.
37+
' Multiply your amplitude in *whole units* and divide by 1000
38+
' to scale back:
39+
' AMP = 9
40+
' OFFSET = 10
41+
' ISIN_DEG = X * 18 ' 18 deg/col → 2 cycles over 40 cols
42+
' GOSUB isin
43+
' Y = OFFSET + (AMP * ISIN_RESULT) / 1000
44+
'
45+
' Range:
46+
' ISIN_RESULT in -1000..1000
47+
' ICOS_RESULT in -1000..1000
48+
' ISIN_DEG / ICOS_DEG accept any signed int (wraps mod 360)
49+
'
50+
' Why a LUT instead of SIN()?
51+
' rgc-basic native uses IEEE float SIN; ugBASIC uses per-
52+
' target fixed-point. Same source code produces different
53+
' curves. Authoring the LUT explicitly gives identical
54+
' output everywhere.
55+
'
56+
' Why GOSUB instead of FUNCTION?
57+
' ugBASIC has PROCEDURE not FUNCTION; rgc-basic has
58+
' FUNCTION. GOSUB + global I/O variables is the lowest-
59+
' common-denominator pattern that works in both without a
60+
' transpiler rewrite.
61+
' ============================================================
62+
63+
' Storage: 360 entries, each in -100..100 → SIGNED WORD (16-bit
64+
' signed) is the safe minimum on ugBASIC. SIGNED BYTE (-128..127)
65+
' would technically fit but mixed-type arithmetic later wants WORD.
66+
DIM SIN_LUT(359) AS SIGNED WORD
67+
68+
' I/O variables must be SIGNED WORD too — degrees can exceed 255
69+
' (BYTE max) once you start scaling X * deg-per-col, and the result
70+
' is ±100 which fits BYTE but the multiplications don't.
71+
DIM ISIN_DEG AS SIGNED WORD
72+
DIM ISIN_D AS SIGNED WORD
73+
DIM ISIN_RESULT AS SIGNED WORD
74+
DIM ICOS_DEG AS SIGNED WORD
75+
DIM ICOS_D AS SIGNED WORD
76+
DIM ICOS_RESULT AS SIGNED WORD
77+
DIM LUT_I AS SIGNED WORD
78+
79+
' Skip past the subroutine bodies + DATA so they don't execute
80+
' inline when the file is #INCLUDE'd at the top of a program.
81+
' Subroutines are reached only via GOSUB; falling through into them
82+
' would hit RETURN with no call stack and halt the program.
83+
GOTO trig_lut_skip
84+
85+
' ---------- init_trig_lut ----------
86+
' Read 360 sine values * 100 from DATA below into SIN_LUT.
87+
' Call once at startup before any ISIN / ICOS use.
88+
init_trig_lut:
89+
FOR LUT_I = 0 TO 359
90+
READ SIN_LUT(LUT_I)
91+
NEXT LUT_I
92+
RETURN
93+
94+
' ---------- isin ----------
95+
' Input: ISIN_DEG (signed integer degrees)
96+
' Output: ISIN_RESULT (sine * 100, range -100..100)
97+
isin:
98+
ISIN_D = ISIN_DEG MOD 360
99+
IF ISIN_D < 0 THEN ISIN_D = ISIN_D + 360
100+
ISIN_RESULT = SIN_LUT(ISIN_D)
101+
RETURN
102+
103+
' ---------- icos ----------
104+
' Input: ICOS_DEG
105+
' Output: ICOS_RESULT
106+
' cos(x) = sin(x + 90), so reuse the table with a shift.
107+
icos:
108+
ICOS_D = (ICOS_DEG + 90) MOD 360
109+
IF ICOS_D < 0 THEN ICOS_D = ICOS_D + 360
110+
ICOS_RESULT = SIN_LUT(ICOS_D)
111+
RETURN
112+
113+
' --- 360-entry sine table, value * 1000, indexed by degree --------
114+
' Generated by: python3 -c "import math; print(','.join(str(int(round(1000*math.sin(math.radians(d))))) for d in range(360)))"
115+
' ×1000 scale gives 10× resolution vs ×100, smoothing visible
116+
' clustering when scaling to small amplitudes (sprite motion math).
117+
' Range fits SIGNED WORD (±1000 << 32767).
118+
119+
DATA 0,17,35,52,70,87,105,122,139,156,174,191,208,225,242,259,276,292,309,326,342,358,375,391,407,423,438,454,469,485
120+
DATA 500,515,530,545,559,574,588,602,616,629,643,656,669,682,695,707,719,731,743,755,766,777,788,799,809,819,829,839,848,857
121+
DATA 866,875,883,891,899,906,914,921,927,934,940,946,951,956,961,966,970,974,978,982,985,988,990,993,995,996,998,999,999,1000
122+
DATA 1000,1000,999,999,998,996,995,993,990,988,985,982,978,974,970,966,961,956,951,946,940,934,927,921,914,906,899,891,883,875
123+
DATA 866,857,848,839,829,819,809,799,788,777,766,755,743,731,719,707,695,682,669,656,643,629,616,602,588,574,559,545,530,515
124+
DATA 500,485,469,454,438,423,407,391,375,358,342,326,309,292,276,259,242,225,208,191,174,156,139,122,105,87,70,52,35,17
125+
DATA 0,-17,-35,-52,-70,-87,-105,-122,-139,-156,-174,-191,-208,-225,-242,-259,-276,-292,-309,-326,-342,-358,-375,-391,-407,-423,-438,-454,-469,-485
126+
DATA -500,-515,-530,-545,-559,-574,-588,-602,-616,-629,-643,-656,-669,-682,-695,-707,-719,-731,-743,-755,-766,-777,-788,-799,-809,-819,-829,-839,-848,-857
127+
DATA -866,-875,-883,-891,-899,-906,-914,-921,-927,-934,-940,-946,-951,-956,-961,-966,-970,-974,-978,-982,-985,-988,-990,-993,-995,-996,-998,-999,-999,-1000
128+
DATA -1000,-1000,-999,-999,-998,-996,-995,-993,-990,-988,-985,-982,-978,-974,-970,-966,-961,-956,-951,-946,-940,-934,-927,-921,-914,-906,-899,-891,-883,-875
129+
DATA -866,-857,-848,-839,-829,-819,-809,-799,-788,-777,-766,-755,-743,-731,-719,-707,-695,-682,-669,-656,-643,-629,-616,-602,-588,-574,-559,-545,-530,-515
130+
DATA -500,-485,-469,-454,-438,-423,-407,-391,-375,-358,-342,-326,-309,-292,-276,-259,-242,-225,-208,-191,-174,-156,-139,-122,-105,-87,-70,-52,-35,-17
131+
132+
' Skip-target: GOTO at the top of this file lands here so the
133+
' main program (the file that #INCLUDE'd this one) continues
134+
' execution past the LUT internals.
135+
trig_lut_skip:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
10 PRINT "NUMBERED"
2+
PRINT "UNNUMBERED"
3+
20 GOTO 10

tests/portable/numbered_lines.bas

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
10 PRINT "ONE"
2+
20 GOTO 50
3+
30 PRINT "SKIPPED"
4+
40 PRINT "ALSO SKIPPED"
5+
50 PRINT "TWO"
6+
60 GOSUB 100
7+
70 END
8+
100 PRINT "SUB"
9+
110 RETURN

tests/transpile/hello.expected.ugb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
REM @include c64
22
REM transpiled from rgc-basic by rgc2ugb v0
33

4-
REM Hello world — pure portable subset.
4+
REM hello world — pure portable subset.
55
PRINT "HELLO, WORLD"
6-
END
6+
END

tests/transpile/loop_counter.expected.ugb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ REM @include c64
22
REM transpiled from rgc-basic by rgc2ugb v0
33

44
REM FOR / NEXT / IF / ELSE IF — all portable.
5-
FOR I = 1 TO 10
6-
IF I MOD 2 = 0 THEN
7-
PRINT I; " EVEN"
8-
ELSEIF I = 1 THEN
9-
PRINT I; " ONE"
5+
FOR i = 1 TO 10
6+
IF i MOD 2 = 0 THEN
7+
PRINT i; " EVEN"
8+
ELSEIF i = 1 THEN
9+
PRINT i; " ONE"
1010
ELSE
11-
PRINT I; " ODD"
11+
PRINT i; " ODD"
1212
ENDIF
13-
NEXT I
14-
END
13+
NEXT i
14+
END
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
REM @include c64
2+
REM transpiled from rgc-basic by rgc2ugb v0
3+
4+
PRINT "ONE"
5+
GOTO 50
6+
PRINT "SKIPPED"
7+
PRINT "ALSO SKIPPED"
8+
50 PRINT "TWO"
9+
GOSUB 100
10+
END
11+
100 PRINT "SUB"
12+
RETURN

tests/transpile/sprite_simple.expected.ugb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ REM transpiled from rgc-basic by rgc2ugb v0
44
REM SCREEN 1 + SPRITE LOAD + SPRITE DRAW — portable surface.
55
BITMAP ENABLE (16)
66
PAPER 6
7+
EMPTY TILE = 32
78
CLS
89
_img_0 = LOAD IMAGE("ship.png")
910
_spr_0 = SPRITE(_img_0)
1011
SPRITE _spr_0 ENABLE
11-
X = 0
12+
x = 0
1213
DO
1314
IF KEY STATE(81) THEN EXIT
14-
SPRITE _spr_0 AT X, 100
15-
X = X + 1
16-
IF X > 320 THEN X = 0
15+
SPRITE _spr_0 AT x, 100
16+
x = x + 1
17+
IF x > 320 THEN x = 0
1718
WAIT VBL
1819
LOOP
19-
END
20+
END
26 Bytes
Binary file not shown.
10.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)