Skip to content

Commit 06b229f

Browse files
committed
feat(basic): compile-time #IF TARGET / #ELSE conditionals
Add C-style conditional compilation. `#IF TARGET <id[,id...]>` selects a branch by machine id; `#ELSEIF`/`#ELSE`/`#END IF` complete the chain alongside the existing `#IF MODERN|RETRO|PORTABLE` tier axis. The non-matching branch is dropped, so per-platform code costs nothing on machines that skip it (unlike a runtime IF, which ships every branch). - directives.py: TARGET conditions, #ELSE/#ELSEIF, new additive `target=` kwarg (keeps the private rgc-transpiler sibling import working). Restructured so directives inside a dropped branch are suppressed. - basic.c: interpreter now honours #IF at load time instead of dying with "Unknown directive" (exit 1). New --target / --tier flags; default target "native", tier "modern". Kept in sync with directives.py. - test_directives.py: 15 cases for target/tier/else/elseif + line stability. - examples/conditional-target.bas: demo. Verified: make check, directive tests, native + gfx + wasm builds.
1 parent 57ba789 commit 06b229f

4 files changed

Lines changed: 474 additions & 23 deletions

File tree

basic.c

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,87 @@ static int str_eq_ci(const char *a, const char *b)
32803280
return (*a == *b);
32813281
}
32823282

3283+
/* Active build identity for compile-time #IF blocks during load.
3284+
* The interpreter is a modern/native host, so it keeps MODERN + PORTABLE
3285+
* (tier) blocks by default and only keeps a `#IF TARGET <id>` block when
3286+
* --target names that id. This mirrors the transpiler's per-target strip,
3287+
* so a .bas with per-platform branches runs here the way it would compile
3288+
* for the chosen machine. Kept in sync with tools/rgc_lint/directives.py. */
3289+
static char active_target[32] = "native";
3290+
static char active_tier[16] = "modern";
3291+
3292+
/* Compare one comma-separated id list entry (case-insensitive). Returns 1
3293+
* if `active_target` equals a token in [ids .. end-of-string). */
3294+
static int target_in_list(const char *ids)
3295+
{
3296+
size_t tlen = strlen(active_target);
3297+
while (*ids) {
3298+
const char *start;
3299+
size_t n;
3300+
while (*ids == ' ' || *ids == '\t' || *ids == ',') ids++;
3301+
start = ids;
3302+
while (*ids && *ids != ',' && *ids != ' ' && *ids != '\t') ids++;
3303+
n = (size_t)(ids - start);
3304+
if (n == tlen) {
3305+
size_t k;
3306+
for (k = 0; k < n; k++) {
3307+
if (toupper((unsigned char)start[k]) !=
3308+
toupper((unsigned char)active_target[k])) break;
3309+
}
3310+
if (k == n) return 1;
3311+
}
3312+
}
3313+
return 0;
3314+
}
3315+
3316+
/* If `s` starts with keyword `kw` (case-insensitive) followed by end-of-
3317+
* string or whitespace, return a pointer to the first non-space char after
3318+
* `kw`; otherwise NULL. Used to parse #IF/#ELSEIF/#ELSE/#END IF. */
3319+
static const char *dir_after_kw(const char *s, const char *kw)
3320+
{
3321+
size_t i = 0;
3322+
while (kw[i]) {
3323+
if (toupper((unsigned char)s[i]) != toupper((unsigned char)kw[i])) return NULL;
3324+
i++;
3325+
}
3326+
if (s[i] != '\0' && s[i] != ' ' && s[i] != '\t') return NULL;
3327+
while (s[i] == ' ' || s[i] == '\t') i++;
3328+
return s + i;
3329+
}
3330+
3331+
/* Evaluate an #IF / #ELSEIF condition. `cond` points at the condition text
3332+
* (leading whitespace already skipped, no trailing keyword). */
3333+
static int directive_cond_matches(const char *cond)
3334+
{
3335+
while (*cond == ' ' || *cond == '\t') cond++;
3336+
if (toupper((unsigned char)cond[0]) == 'T' &&
3337+
toupper((unsigned char)cond[1]) == 'A' &&
3338+
toupper((unsigned char)cond[2]) == 'R' &&
3339+
toupper((unsigned char)cond[3]) == 'G' &&
3340+
toupper((unsigned char)cond[4]) == 'E' &&
3341+
toupper((unsigned char)cond[5]) == 'T' &&
3342+
(cond[6] == ' ' || cond[6] == '\t')) {
3343+
return target_in_list(cond + 6);
3344+
}
3345+
if (str_eq_ci(cond, "PORTABLE")) return 1;
3346+
if (str_eq_ci(cond, "MODERN")) return str_eq_ci(active_tier, "modern");
3347+
if (str_eq_ci(cond, "RETRO")) return str_eq_ci(active_tier, "portable");
3348+
/* Unknown condition: keep (matches the Python preprocessor default). */
3349+
return 1;
3350+
}
3351+
3352+
static void set_active_target(const char *v)
3353+
{
3354+
strncpy(active_target, v, sizeof(active_target) - 1);
3355+
active_target[sizeof(active_target) - 1] = '\0';
3356+
}
3357+
3358+
static void set_active_tier(const char *v)
3359+
{
3360+
strncpy(active_tier, v, sizeof(active_tier) - 1);
3361+
active_tier[sizeof(active_tier) - 1] = '\0';
3362+
}
3363+
32833364
/* Apply option from #OPTION directive (file overrides CLI). Returns 0 on success, -1 on error. */
32843365
static int apply_option_directive(const char *name, const char *value)
32853366
{
@@ -20630,6 +20711,12 @@ static void load_file_into_program(const char *path, const char *base_dir, int i
2063020711
int number;
2063120712
const char *resolved;
2063220713
int i;
20714+
/* Compile-time #IF/#ELSEIF/#ELSE/#END IF state for THIS file. Fresh per
20715+
* invocation, so an #INCLUDEd file gets its own chain (same as the Python
20716+
* preprocessor, which recurses per file). */
20717+
int if_in_block = 0; /* inside an #IF ... #END IF chain */
20718+
int if_branch_taken = 0; /* some branch in this chain already matched */
20719+
int if_keep = 1; /* keep lines in the current branch */
2063320720
/* Snapshot base_dir into a stack-local buffer. The caller's `base_dir`
2063420721
* may alias get_base_dir's static buffer, which our recursive
2063520722
* #INCLUDE call would overwrite — corrupting the parent's view of
@@ -20744,6 +20831,78 @@ static void load_file_into_program(const char *path, const char *base_dir, int i
2074420831
continue;
2074520832
}
2074620833

20834+
/* Compile-time conditionals: #IF / #ELSEIF / #ELSE / #END IF.
20835+
* Only the '#'-prefixed spelling is a directive; a bare IF/ELSE is a
20836+
* BASIC statement. Chain-control lines are processed even inside a
20837+
* dropped branch so the state machine stays correct; the interpreter
20838+
* keeps the matching branch and drops the rest (see
20839+
* directive_cond_matches / active_target). Mirrors the Python
20840+
* preprocessor in tools/rgc_lint/directives.py. */
20841+
if (p[0] == '#') {
20842+
const char *d = p + 1;
20843+
const char *rest;
20844+
while (*d == ' ' || *d == '\t') d++;
20845+
/* #ELSEIF / #ELSE IF <cond> — before #ELSE and #IF. */
20846+
if ((rest = dir_after_kw(d, "ELSEIF")) != NULL ||
20847+
((rest = dir_after_kw(d, "ELSE")) != NULL &&
20848+
(rest = dir_after_kw(rest, "IF")) != NULL)) {
20849+
*first_line_seen = 1;
20850+
if (!if_in_block) {
20851+
fprintf(stderr, "#ELSEIF without #IF\n");
20852+
} else if (if_branch_taken) {
20853+
if_keep = 0;
20854+
} else {
20855+
if_keep = directive_cond_matches(rest);
20856+
if (if_keep) if_branch_taken = 1;
20857+
}
20858+
free(linebuf);
20859+
continue;
20860+
}
20861+
/* #ELSE (exact). */
20862+
if ((rest = dir_after_kw(d, "ELSE")) != NULL && *rest == '\0') {
20863+
*first_line_seen = 1;
20864+
if (!if_in_block) {
20865+
fprintf(stderr, "#ELSE without #IF\n");
20866+
} else {
20867+
if_keep = !if_branch_taken;
20868+
if_branch_taken = 1;
20869+
}
20870+
free(linebuf);
20871+
continue;
20872+
}
20873+
/* #END IF / #ENDIF. */
20874+
if (((rest = dir_after_kw(d, "END")) != NULL &&
20875+
(rest = dir_after_kw(rest, "IF")) != NULL) ||
20876+
((rest = dir_after_kw(d, "ENDIF")) != NULL)) {
20877+
*first_line_seen = 1;
20878+
if_in_block = 0;
20879+
if_branch_taken = 0;
20880+
if_keep = 1;
20881+
free(linebuf);
20882+
continue;
20883+
}
20884+
/* #IF <cond>. */
20885+
if ((rest = dir_after_kw(d, "IF")) != NULL) {
20886+
*first_line_seen = 1;
20887+
if (if_in_block) {
20888+
fprintf(stderr, "nested #IF not supported\n");
20889+
} else {
20890+
if_in_block = 1;
20891+
if_keep = directive_cond_matches(rest);
20892+
if_branch_taken = if_keep;
20893+
}
20894+
free(linebuf);
20895+
continue;
20896+
}
20897+
}
20898+
20899+
/* Dropped branch: suppress every other line (blank, code, #OPTION,
20900+
* #INCLUDE) until the next chain-control directive. */
20901+
if (if_in_block && !if_keep) {
20902+
free(linebuf);
20903+
continue;
20904+
}
20905+
2074720906
/* Meta directives: #OPTION / #INCLUDE, or OPTION / INCLUDE (IDE / 8bitworkshop style, no #). */
2074820907
{
2074920908
int meta = 0;
@@ -21342,6 +21501,18 @@ int basic_parse_arg_flags(int argc, char **argv, int start, int expect_program_p
2134221501
return -1;
2134321502
}
2134421503
#endif
21504+
} else if (strcmp(argv[i], "-target") == 0 || strcmp(argv[i], "--target") == 0) {
21505+
if (i + 1 >= argc) {
21506+
fprintf(stderr, "Missing value for --target\n");
21507+
return -1;
21508+
}
21509+
set_active_target(argv[++i]);
21510+
} else if (strcmp(argv[i], "-tier") == 0 || strcmp(argv[i], "--tier") == 0) {
21511+
if (i + 1 >= argc) {
21512+
fprintf(stderr, "Missing value for --tier\n");
21513+
return -1;
21514+
}
21515+
set_active_tier(argv[++i]);
2134521516
} else if (argv[i][0] == '-') {
2134621517
fprintf(stderr, "Unknown option: %s\n", argv[i]);
2134721518
return -1;
@@ -21815,7 +21986,7 @@ int main(int argc, char **argv)
2181521986
}
2181621987

2181721988
if (argc < 2) {
21818-
fprintf(stderr, "Usage: %s [-v|--version] [-petscii] [-petscii-plain] [-charset upper|lower|c64-*|pet-*] [-palette ansi|c64] [-maxstr N] [-columns N] [-nowrap] [-wrap] [-diagnostics] [-json-status] <program.bas>\n", argv[0]);
21989+
fprintf(stderr, "Usage: %s [-v|--version] [-petscii] [-petscii-plain] [-charset upper|lower|c64-*|pet-*] [-palette ansi|c64] [-maxstr N] [-columns N] [-target ID] [-tier modern|portable] [-nowrap] [-wrap] [-diagnostics] [-json-status] <program.bas>\n", argv[0]);
2181921990
return 1;
2182021991
}
2182121992

@@ -21924,9 +22095,21 @@ int main(int argc, char **argv)
2192422095
json_status_mode = 1;
2192522096
} else if (strcmp(argv[i], "-trace") == 0 || strcmp(argv[i], "--trace") == 0) {
2192622097
trace_enabled = 1;
22098+
} else if (strcmp(argv[i], "-target") == 0 || strcmp(argv[i], "--target") == 0) {
22099+
if (i + 1 >= argc) {
22100+
fprintf(stderr, "Missing value for --target\n");
22101+
return 1;
22102+
}
22103+
set_active_target(argv[++i]);
22104+
} else if (strcmp(argv[i], "-tier") == 0 || strcmp(argv[i], "--tier") == 0) {
22105+
if (i + 1 >= argc) {
22106+
fprintf(stderr, "Missing value for --tier\n");
22107+
return 1;
22108+
}
22109+
set_active_tier(argv[++i]);
2192722110
} else if (argv[i][0] == '-') {
2192822111
fprintf(stderr, "Unknown option: %s\n", argv[i]);
21929-
fprintf(stderr, "Usage: %s [-petscii] [-petscii-plain] [-charset upper|lower|c64-*|pet-*] [-palette ansi|c64] [-maxstr N] [-columns N] [-nowrap] [-wrap] [-trace] [-diagnostics] [-json-status] <program.bas>\n", argv[0]);
22112+
fprintf(stderr, "Usage: %s [-petscii] [-petscii-plain] [-charset upper|lower|c64-*|pet-*] [-palette ansi|c64] [-maxstr N] [-columns N] [-target ID] [-tier modern|portable] [-nowrap] [-wrap] [-trace] [-diagnostics] [-json-status] <program.bas>\n", argv[0]);
2193022113
return 1;
2193122114
} else {
2193222115
prog_path = argv[i];

examples/conditional-target.bas

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
REM Compile-time platform conditionals — the RGC-BASIC answer to #ifdef.
2+
REM
3+
REM #IF TARGET <id[,id...]> / #ELSEIF / #ELSE / #END IF select code at
4+
REM load (interpreter) or transpile time. The non-matching branches are
5+
REM dropped entirely, so per-machine code costs nothing on the machines
6+
REM that do not use it — unlike a runtime IF, which ships every branch.
7+
REM
8+
REM Run it different ways:
9+
REM basic examples/conditional-target.bas (target = native)
10+
REM basic --target c64 examples/conditional-target.bas
11+
REM basic --target zxspectrum examples/conditional-target.bas
12+
REM
13+
REM The tier axis (#IF MODERN / RETRO / PORTABLE) still works alongside
14+
REM TARGET; --tier modern|portable picks it (interpreter default: modern).
15+
16+
PRINT "Border setup:"
17+
18+
#IF TARGET c64,c128
19+
PRINT " POKE 53280,0 : REM VIC-II border black"
20+
#ELSEIF TARGET zxspectrum
21+
PRINT " BORDER 0 : REM ULA border black"
22+
#ELSE
23+
PRINT " (no hardware border on this machine)"
24+
#END IF
25+
26+
#IF MODERN
27+
PRINT "Modern host: sound, files, and full-width screen available."
28+
#END IF
29+
30+
#IF RETRO
31+
PRINT "Retro build: keep it small, assume 40 columns or fewer."
32+
#END IF
33+
34+
PRINT "Done."

0 commit comments

Comments
 (0)