Phase 3: file I/O, PRINT USING, SAVE/LOAD, MID$ assignment, graphics stubs
Add OPEN/CLOSE with both modern (OPEN "f" FOR OUTPUT AS #n) and compact (OPEN "O",#n,"f") syntaxes. PRINT#, WRITE#, INPUT#, LINE INPUT# for sequential file access. EOF() now returns real file status with peek-ahead. LOC/LOF return approximate values. PRINT USING with numeric (#, ., +, -, $$, **, ^^^^, comma grouping) and string (!, &, \ \) format specifiers. Shared by PRINT USING and PRINT# USING. SAVE (ASCII), LOAD (with ,R auto-run), and MERGE for program persistence. MID$ assignment (MID$(var$, start [,len]) = expr) for in-place string modification. Works with both scalar variables and array elements. Graphics stubs for SCREEN, PSET, PRESET, LINE, CIRCLE, DRAW, PAINT, VIEW, WINDOW, PALETTE - parse and discard arguments so graphics-heavy programs don't crash. SYSTEM and NEW/CLEAR now close all open files. Version bumped to 0.3.0. 22 tests pass (16 existing + 6 new).
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
#include "gwbasic.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <ctype.h>
|
||||
|
||||
/*
|
||||
* PRINT USING formatter - implements GW-BASIC's formatted output.
|
||||
*
|
||||
* Numeric format characters:
|
||||
* # Digit position
|
||||
* . Decimal point
|
||||
* , Insert commas every 3 digits (before decimal)
|
||||
* + Print sign (at start or end)
|
||||
* - Trailing negative sign
|
||||
* $$ Leading dollar sign
|
||||
* ** Asterisk fill
|
||||
* **$ Asterisk fill + dollar
|
||||
* ^^^^ Scientific notation
|
||||
*
|
||||
* String format characters:
|
||||
* ! First character only
|
||||
* & Entire string
|
||||
* \ \ Fixed width (count chars between backslashes inclusive)
|
||||
*
|
||||
* _ Literal escape: next char printed as-is
|
||||
*/
|
||||
|
||||
/* Output helpers */
|
||||
static void pu_putch(FILE *fp, int ch)
|
||||
{
|
||||
if (fp) {
|
||||
fputc(ch, fp);
|
||||
} else if (gw_hal) {
|
||||
gw_hal->putch(ch);
|
||||
} else {
|
||||
putchar(ch);
|
||||
}
|
||||
}
|
||||
|
||||
static void pu_puts(FILE *fp, const char *s)
|
||||
{
|
||||
while (*s)
|
||||
pu_putch(fp, *s++);
|
||||
}
|
||||
|
||||
/* Format a numeric value according to the format spec */
|
||||
static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
|
||||
{
|
||||
/* Parse the format spec */
|
||||
bool leading_plus = false, trailing_plus = false;
|
||||
bool trailing_minus = false;
|
||||
bool dollar = false;
|
||||
bool asterisk_fill = false;
|
||||
bool use_commas = false;
|
||||
bool scientific = false;
|
||||
int total_digits = 0; /* total # positions */
|
||||
int decimal_digits = -1; /* -1 = no decimal point */
|
||||
bool has_decimal = false;
|
||||
|
||||
int i = 0;
|
||||
|
||||
/* Check leading + */
|
||||
if (i < fmtlen && fmt[i] == '+') {
|
||||
leading_plus = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
/* Check ** or **$ or $$ */
|
||||
if (i + 1 < fmtlen && fmt[i] == '*' && fmt[i + 1] == '*') {
|
||||
asterisk_fill = true;
|
||||
total_digits += 2;
|
||||
i += 2;
|
||||
if (i < fmtlen && fmt[i] == '$') {
|
||||
dollar = true;
|
||||
i++;
|
||||
total_digits++;
|
||||
}
|
||||
} else if (i + 1 < fmtlen && fmt[i] == '$' && fmt[i + 1] == '$') {
|
||||
dollar = true;
|
||||
total_digits += 2;
|
||||
i += 2;
|
||||
}
|
||||
|
||||
/* Count # before decimal */
|
||||
while (i < fmtlen && fmt[i] == '#') {
|
||||
total_digits++;
|
||||
i++;
|
||||
}
|
||||
|
||||
/* Check for comma */
|
||||
if (i < fmtlen && fmt[i] == ',') {
|
||||
use_commas = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
/* More # after comma but before decimal */
|
||||
while (i < fmtlen && fmt[i] == '#') {
|
||||
total_digits++;
|
||||
i++;
|
||||
}
|
||||
|
||||
/* Decimal point */
|
||||
if (i < fmtlen && fmt[i] == '.') {
|
||||
has_decimal = true;
|
||||
decimal_digits = 0;
|
||||
i++;
|
||||
while (i < fmtlen && fmt[i] == '#') {
|
||||
decimal_digits++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scientific notation (^^^^) */
|
||||
int caret_count = 0;
|
||||
while (i < fmtlen && fmt[i] == '^') {
|
||||
caret_count++;
|
||||
i++;
|
||||
}
|
||||
if (caret_count >= 4)
|
||||
scientific = true;
|
||||
|
||||
/* Trailing sign */
|
||||
if (i < fmtlen && fmt[i] == '+') {
|
||||
trailing_plus = true;
|
||||
i++;
|
||||
} else if (i < fmtlen && fmt[i] == '-') {
|
||||
trailing_minus = true;
|
||||
i++;
|
||||
}
|
||||
|
||||
/* Format the number */
|
||||
bool negative = val < 0;
|
||||
double absval = fabs(val);
|
||||
|
||||
if (scientific) {
|
||||
int dec = decimal_digits >= 0 ? decimal_digits : 0;
|
||||
char numbuf[64];
|
||||
snprintf(numbuf, sizeof(numbuf), "%+.*E", dec, val);
|
||||
|
||||
/* Reformat to match GW-BASIC: sign, digits, E+nn */
|
||||
int field_width = total_digits + (has_decimal ? 1 : 0) + 5; /* E+nn + sign */
|
||||
if (leading_plus || trailing_plus || trailing_minus)
|
||||
field_width++;
|
||||
|
||||
char outbuf[64];
|
||||
int oi = 0;
|
||||
if (leading_plus)
|
||||
outbuf[oi++] = negative ? '-' : '+';
|
||||
else if (!trailing_plus && !trailing_minus)
|
||||
outbuf[oi++] = negative ? '-' : ' ';
|
||||
|
||||
/* Format mantissa */
|
||||
char mantissa[32];
|
||||
int exp_val;
|
||||
if (val == 0) {
|
||||
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, 0.0);
|
||||
exp_val = 0;
|
||||
} else {
|
||||
exp_val = (int)floor(log10(absval));
|
||||
double mant = absval / pow(10, exp_val);
|
||||
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
|
||||
/* Check for rounding overflow (e.g., 9.95 -> 10.0 with 1 dec) */
|
||||
if (mantissa[0] != '0' && (mantissa[0] - '0') >= 10) {
|
||||
exp_val++;
|
||||
mant = absval / pow(10, exp_val);
|
||||
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; mantissa[j] && oi < (int)sizeof(outbuf) - 8; j++)
|
||||
outbuf[oi++] = mantissa[j];
|
||||
|
||||
snprintf(outbuf + oi, sizeof(outbuf) - oi, "E%+03d", exp_val);
|
||||
oi = strlen(outbuf);
|
||||
|
||||
if (trailing_plus)
|
||||
outbuf[oi++] = negative ? '-' : '+';
|
||||
else if (trailing_minus)
|
||||
outbuf[oi++] = negative ? '-' : ' ';
|
||||
outbuf[oi] = '\0';
|
||||
|
||||
pu_puts(fp, outbuf);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Fixed-point formatting */
|
||||
int dec = (decimal_digits >= 0) ? decimal_digits : 0;
|
||||
char numbuf[64];
|
||||
snprintf(numbuf, sizeof(numbuf), "%.*f", dec, absval);
|
||||
|
||||
/* Split into integer and fractional parts */
|
||||
char *dot = strchr(numbuf, '.');
|
||||
char intpart[48], fracpart[48];
|
||||
if (dot) {
|
||||
int ilen = dot - numbuf;
|
||||
memcpy(intpart, numbuf, ilen);
|
||||
intpart[ilen] = '\0';
|
||||
strcpy(fracpart, dot + 1);
|
||||
} else {
|
||||
strcpy(intpart, numbuf);
|
||||
fracpart[0] = '\0';
|
||||
}
|
||||
|
||||
/* Handle commas */
|
||||
char int_with_commas[80];
|
||||
if (use_commas) {
|
||||
int ilen = strlen(intpart);
|
||||
int oi = 0;
|
||||
for (int j = 0; j < ilen; j++) {
|
||||
if (j > 0 && (ilen - j) % 3 == 0)
|
||||
int_with_commas[oi++] = ',';
|
||||
int_with_commas[oi++] = intpart[j];
|
||||
}
|
||||
int_with_commas[oi] = '\0';
|
||||
} else {
|
||||
strcpy(int_with_commas, intpart);
|
||||
}
|
||||
|
||||
/* Build the number string without sign */
|
||||
char numstr[160];
|
||||
if (has_decimal)
|
||||
snprintf(numstr, sizeof(numstr), "%s.%s", int_with_commas, fracpart);
|
||||
else
|
||||
snprintf(numstr, sizeof(numstr), "%s", int_with_commas);
|
||||
|
||||
/* Calculate total field width for integer part */
|
||||
int int_field = total_digits;
|
||||
int total_field = int_field + (has_decimal ? 1 + dec : 0);
|
||||
|
||||
/* Pad the number to fill the field */
|
||||
char result[80];
|
||||
int numlen = strlen(numstr);
|
||||
int padding = total_field - numlen;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
int ri = 0;
|
||||
|
||||
/* Sign handling */
|
||||
bool sign_placed = false;
|
||||
if (leading_plus) {
|
||||
result[ri++] = negative ? '-' : '+';
|
||||
sign_placed = true;
|
||||
}
|
||||
|
||||
/* Fill padding */
|
||||
char fillch = asterisk_fill ? '*' : ' ';
|
||||
bool dollar_placed = false;
|
||||
|
||||
if (numlen > total_field) {
|
||||
/* Overflow: print % followed by the number */
|
||||
pu_putch(fp, '%');
|
||||
if (!sign_placed && !trailing_plus && !trailing_minus) {
|
||||
if (negative) pu_putch(fp, '-');
|
||||
}
|
||||
pu_puts(fp, numstr);
|
||||
if (trailing_plus)
|
||||
pu_putch(fp, negative ? '-' : '+');
|
||||
else if (trailing_minus)
|
||||
pu_putch(fp, negative ? '-' : ' ');
|
||||
return;
|
||||
}
|
||||
|
||||
for (int j = 0; j < padding; j++) {
|
||||
if (dollar && !dollar_placed && j == padding - 1) {
|
||||
if (!sign_placed && negative && !trailing_plus && !trailing_minus) {
|
||||
result[ri++] = fillch == '*' ? '*' : ' ';
|
||||
}
|
||||
result[ri++] = '$';
|
||||
dollar_placed = true;
|
||||
} else {
|
||||
result[ri++] = fillch;
|
||||
}
|
||||
}
|
||||
|
||||
/* Place sign if not yet placed and no trailing sign */
|
||||
if (!sign_placed && !trailing_plus && !trailing_minus) {
|
||||
if (negative) {
|
||||
/* Find rightmost fill char and replace with - */
|
||||
bool placed = false;
|
||||
for (int j = ri - 1; j >= 0; j--) {
|
||||
if (result[j] == ' ' || (result[j] == '*' && !asterisk_fill)) {
|
||||
result[j] = '-';
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed && ri > 0) {
|
||||
/* Put sign before number */
|
||||
if (result[0] == ' ' || result[0] == '*')
|
||||
result[0] = '-';
|
||||
else {
|
||||
/* Shift everything right */
|
||||
memmove(result + 1, result, ri);
|
||||
result[0] = '-';
|
||||
ri++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dollar sign if not placed yet */
|
||||
if (dollar && !dollar_placed) {
|
||||
if (ri > 0 && (result[ri - 1] == ' ' || result[ri - 1] == '*'))
|
||||
result[ri - 1] = '$';
|
||||
else
|
||||
result[ri++] = '$';
|
||||
}
|
||||
|
||||
/* Copy the number digits */
|
||||
for (int j = 0; j < numlen && ri < (int)sizeof(result) - 4; j++)
|
||||
result[ri++] = numstr[j];
|
||||
|
||||
/* Trailing sign */
|
||||
if (trailing_plus)
|
||||
result[ri++] = negative ? '-' : '+';
|
||||
else if (trailing_minus)
|
||||
result[ri++] = negative ? '-' : ' ';
|
||||
result[ri] = '\0';
|
||||
|
||||
pu_puts(fp, result);
|
||||
}
|
||||
|
||||
/* Format a string value */
|
||||
static void format_string(FILE *fp, const char *fmt, int fmtlen, gw_string_t *s)
|
||||
{
|
||||
if (fmtlen == 1 && fmt[0] == '!') {
|
||||
/* First character only */
|
||||
if (s->len > 0)
|
||||
pu_putch(fp, s->data[0]);
|
||||
else
|
||||
pu_putch(fp, ' ');
|
||||
} else if (fmtlen == 1 && fmt[0] == '&') {
|
||||
/* Entire string */
|
||||
for (int i = 0; i < s->len; i++)
|
||||
pu_putch(fp, s->data[i]);
|
||||
} else if (fmt[0] == '\\') {
|
||||
/* Fixed width: count chars between backslashes, inclusive */
|
||||
int width = fmtlen; /* includes both backslashes */
|
||||
for (int i = 0; i < width; i++) {
|
||||
if (i < s->len)
|
||||
pu_putch(fp, s->data[i]);
|
||||
else
|
||||
pu_putch(fp, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* PRINT USING "format"; expr [, expr ...]
|
||||
* fp: output FILE* (NULL = screen via HAL)
|
||||
* Called after USING token has been consumed.
|
||||
*/
|
||||
void gw_print_using(FILE *fp)
|
||||
{
|
||||
gw_skip_spaces();
|
||||
gw_value_t fmt_val = gw_eval_str();
|
||||
char *fmt = gw_str_to_cstr(&fmt_val.sval);
|
||||
int fmtlen = fmt_val.sval.len;
|
||||
gw_str_free(&fmt_val.sval);
|
||||
|
||||
gw_skip_spaces();
|
||||
if (gw_chrgot() == ';')
|
||||
gw_chrget();
|
||||
|
||||
int fi = 0; /* format index */
|
||||
bool had_semicolon = false;
|
||||
|
||||
for (;;) {
|
||||
gw_skip_spaces();
|
||||
uint8_t tok = gw_chrgot();
|
||||
if (tok == 0 || tok == ':' || tok == TOK_ELSE)
|
||||
break;
|
||||
|
||||
/* Reset format position if we've gone past the end */
|
||||
if (fi >= fmtlen)
|
||||
fi = 0;
|
||||
|
||||
/* Output literal characters until we hit a format spec */
|
||||
while (fi < fmtlen) {
|
||||
char ch = fmt[fi];
|
||||
|
||||
/* Literal escape */
|
||||
if (ch == '_') {
|
||||
fi++;
|
||||
if (fi < fmtlen)
|
||||
pu_putch(fp, fmt[fi++]);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Numeric format start */
|
||||
if (ch == '#' || ch == '+' || ch == '.' ||
|
||||
(ch == '*' && fi + 1 < fmtlen && fmt[fi + 1] == '*') ||
|
||||
(ch == '$' && fi + 1 < fmtlen && fmt[fi + 1] == '$'))
|
||||
break;
|
||||
|
||||
/* String format start */
|
||||
if (ch == '!' || ch == '&' || ch == '\\')
|
||||
break;
|
||||
|
||||
/* Regular character - output as literal */
|
||||
pu_putch(fp, ch);
|
||||
fi++;
|
||||
}
|
||||
|
||||
if (fi >= fmtlen) {
|
||||
/* Check if there are more values to format */
|
||||
gw_skip_spaces();
|
||||
tok = gw_chrgot();
|
||||
if (tok == 0 || tok == ':' || tok == TOK_ELSE)
|
||||
break;
|
||||
/* More values: reset format and re-enter the loop */
|
||||
fi = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Determine format spec type and length */
|
||||
char ch = fmt[fi];
|
||||
|
||||
/* String formats */
|
||||
if (ch == '!') {
|
||||
gw_value_t v = gw_eval_str();
|
||||
format_string(fp, "!", 1, &v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
fi++;
|
||||
} else if (ch == '&') {
|
||||
gw_value_t v = gw_eval_str();
|
||||
format_string(fp, "&", 1, &v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
fi++;
|
||||
} else if (ch == '\\') {
|
||||
/* Count to closing backslash */
|
||||
int start = fi;
|
||||
fi++;
|
||||
while (fi < fmtlen && fmt[fi] != '\\')
|
||||
fi++;
|
||||
if (fi < fmtlen) fi++; /* skip closing backslash */
|
||||
int width = fi - start;
|
||||
gw_value_t v = gw_eval_str();
|
||||
format_string(fp, fmt + start, width, &v.sval);
|
||||
gw_str_free(&v.sval);
|
||||
} else {
|
||||
/* Numeric format - collect the entire spec */
|
||||
int start = fi;
|
||||
|
||||
/* Leading + */
|
||||
if (fi < fmtlen && fmt[fi] == '+') fi++;
|
||||
|
||||
/* ** or **$ or $$ */
|
||||
if (fi + 1 < fmtlen && fmt[fi] == '*' && fmt[fi + 1] == '*') {
|
||||
fi += 2;
|
||||
if (fi < fmtlen && fmt[fi] == '$') fi++;
|
||||
} else if (fi + 1 < fmtlen && fmt[fi] == '$' && fmt[fi + 1] == '$') {
|
||||
fi += 2;
|
||||
}
|
||||
|
||||
/* # digits and commas */
|
||||
while (fi < fmtlen && (fmt[fi] == '#' || fmt[fi] == ','))
|
||||
fi++;
|
||||
|
||||
/* Decimal point and fraction digits */
|
||||
if (fi < fmtlen && fmt[fi] == '.') {
|
||||
fi++;
|
||||
while (fi < fmtlen && fmt[fi] == '#')
|
||||
fi++;
|
||||
}
|
||||
|
||||
/* Carets for scientific notation */
|
||||
while (fi < fmtlen && fmt[fi] == '^')
|
||||
fi++;
|
||||
|
||||
/* Trailing sign */
|
||||
if (fi < fmtlen && (fmt[fi] == '+' || fmt[fi] == '-'))
|
||||
fi++;
|
||||
|
||||
gw_value_t v = gw_eval_num();
|
||||
double dv = gw_to_dbl(&v);
|
||||
format_number(fp, fmt + start, fi - start, dv);
|
||||
}
|
||||
|
||||
/* Check for separator */
|
||||
gw_skip_spaces();
|
||||
had_semicolon = false;
|
||||
if (gw_chrgot() == ';') {
|
||||
gw_chrget();
|
||||
had_semicolon = true;
|
||||
} else if (gw_chrgot() == ',') {
|
||||
gw_chrget();
|
||||
had_semicolon = true;
|
||||
}
|
||||
|
||||
if (!had_semicolon)
|
||||
break;
|
||||
}
|
||||
|
||||
/* Output remaining literal chars from format */
|
||||
while (fi < fmtlen) {
|
||||
if (fmt[fi] == '_') {
|
||||
fi++;
|
||||
if (fi < fmtlen)
|
||||
pu_putch(fp, fmt[fi++]);
|
||||
} else {
|
||||
pu_putch(fp, fmt[fi++]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!had_semicolon)
|
||||
pu_putch(fp, '\n');
|
||||
|
||||
if (fp)
|
||||
fflush(fp);
|
||||
|
||||
free(fmt);
|
||||
}
|
||||
Reference in New Issue
Block a user