diff --git a/docs/getting-started.md b/docs/getting-started.md index ed065a6..ce44d91 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -175,6 +175,63 @@ build/gwbasic-compile --safe -c --runtime . program.bas build/gwbasic-compile --safe=sanitize -c --runtime . program.bas ``` +### Cross-Language Linking (`--emit-obj` / `--main-name`) + +`--emit-obj` produces a `.o` object file instead of a final executable; +`--main-name NAME` renames the entry point so it doesn't collide with +the host project's `main()`. Together they let you link BASIC into a +larger C or Fortran build. + +```bash +# BASIC source compiled to greet.o with renamed entry point +build/gwbasic-compile --emit-obj --main-name=run_basic_greet \ + --runtime . greet.bas +``` + +C driver: + +```c +extern int run_basic_greet(int argc, char **argv); + +int main(void) { + run_basic_greet(0, NULL); /* runs the BASIC program */ + return 0; +} +``` + +Link both together: + +```bash +gcc driver.c greet.o -L./build -lgwrt -lm -lpthread -lpulse-simple +``` + +Fortran driver (modern, with `iso_c_binding`): + +```fortran +program main + use iso_c_binding + interface + function run_basic_greet(argc, argv) bind(c, name="run_basic_greet") + use iso_c_binding + integer(c_int), value :: argc + type(c_ptr), value :: argv + integer(c_int) :: run_basic_greet + end function + end interface + integer(c_int) :: rc + rc = run_basic_greet(0, c_null_ptr) +end program +``` + +```bash +gfortran driver.f90 greet.o -L./build -lgwrt -lm -lpthread -lpulse-simple +``` + +The BASIC code shares the `gw` interpreter state with `libgwrt`, so a +single binary runs at most one BASIC program at a time. Calling BASIC +from C / Fortran is always safe; calling C / Fortran from BASIC needs +the foreign-function-declaration extension on the roadmap (Level 2). + ## Building for DOS / FreeDOS GW-BASIC 2026 cross-compiles to DOS using OpenWatcom V2 (`wcc` / `wcc386`). diff --git a/docs/roadmap.md b/docs/roadmap.md index 29294e3..a79ed0b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -94,15 +94,41 @@ Tested on FreeDOS 1.4 via QEMU. ## Next Up ### Compiler Optimization Flags -- **`--no-gc-check`** -- skip `gwrt_check_line()` per-line calls (no string pool - GC, no Ctrl+Break check) for maximum throughput - **`--inline-arrays`** -- emit direct array indexing for statically-DIMmed arrays instead of runtime `gwrt_array_elem()` lookup -- **`--fast-math`** -- skip division-by-zero checks, allow unsafe float ops - **`-O0` through `-O3`** -- compiler-level optimization tiers mapping to different sets of codegen optimizations (constant folding, dead code elimination, FOR step=1 elision, fast-path expressions) +### Cross-Language Linking + +Three levels of integration with C and Fortran. Level 1 is implemented; +Level 2 is the natural follow-up; Level 3 is deferred unless a concrete +use case appears. + +- **Level 1 -- Link BASIC objects into a larger C/Fortran project (done)** + -- `gwbasic-compile prog.bas --emit-obj --main-name=run_basic` produces + `prog.o` with the entry point renamed. The host project links it + alongside its own objects against `libgwrt`. From Fortran, declare + the entry with `bind(c)`. + +- **Level 2 -- Foreign function declarations from BASIC**: extend the + language with a `'$EXTERN NAME(ARGS) AS TYPE` pragma (or a new + `EXTERNAL` statement) so BASIC code can call C functions directly. + Type mapping: `INTEGER` <-> `int16_t`, `SINGLE` <-> `float`, + `DOUBLE` <-> `double`, `STRING` <-> `char *` (NUL-terminated, owned + by `gw_str_to_cstr`) or `gw_string_t` for richer interop. Fortran + callees must use `bind(c)`; legacy F77/F90 mangling out of scope -- + users write a thin C shim instead. + +- **Level 3 (deferred) -- Embed individual BASIC SUBs/FUNCTIONs as + C-callable functions**. Compile each labeled SUB or DEF FN to a + separate C function with a stable signature; emit a header so C + drivers can call them. Useful when BASIC is the configuration / + rule-engine language for a larger application. Bigger scope: + needs export annotations, header generation, and a way to share + state between calls. Defer until a specific use case appears. + ### IDE Integration - **VS Code extension** -- syntax highlighting (TextMate grammar), snippets, run/debug tasks, integrated terminal runner @@ -110,6 +136,53 @@ Tested on FreeDOS 1.4 via QEMU. run configurations, debugger integration (breakpoints via `STOP`, variable inspection), structure view (line number outline) +### Formatted I/O Extensions + +Beyond the existing `PRINT` / `PRINT USING` / `PRINT#`, expose two +formatted-I/O styles familiar from neighbouring languages. Both write +through the existing HAL output path so they work in interactive mode +and in compiled binaries. + +- **FORTRAN-style `WRITE`** -- `WRITE (#unit, "(format-spec)") args` + with Fortran format-spec language: `I5`, `F8.3`, `E12.4`, `A`, `X`, + `/` (newline), repeat counts, slashes, parenthesized groups. Useful + for porting numerical code; Fortran formats are denser than + PRINT USING. +- **C-style `PRINTF`** -- `PRINTF format$, arg, arg, ...` accepting C's + `%d` / `%f` / `%e` / `%g` / `%s` / `%c` / `%x` / `%o` / width / precision + / flags. Cheaper to learn for users coming from C / Python. Goes to + stdout; `FPRINTF #unit, ...` for file output. + +Both share an underlying formatter (probably a C function in +`libgwrt`) that the codegen calls directly; the interpreter tokenizes +and dispatches the same way. + +### Numerical / Data Standard Library + +Substantial scope -- treat as a separate sub-project, possibly a +companion repo. All three modules build on top of GW-BASIC arrays +(or new dynamically-typed buffers via DEF SEG / virtual memory). +Likely written partly in BASIC and partly in C for the inner loops. + +- **NumPy-style array module** -- `NDARRAY` type with shape, dtype, + broadcasting; element-wise ops (`+`, `*`, `SIN`, `EXP`); reductions + (`SUM`, `MIN`, `MAX`, `MEAN`); slicing; basic linear algebra (`MATMUL`, + `INV`, `EIG`). The existing single-typed BASIC arrays are a starting + point; the new module needs a proper shape/dtype descriptor. +- **DataFrame module (pandas-like)** -- column-oriented table with + named columns and heterogeneous dtypes; CSV / TSV load and save; + filter, sort, group-by, aggregate, join. Builds on the array + module. +- **Plotting module (matplotlib-like)** -- high-level wrappers + (`PLOT x, y`, `SCATTER`, `BAR`, `HIST`) with axes, labels, legend, + title. Backend: existing CGA / Sixel rendering for terminals; PNG + output via stb_image_write or libpng for files; SVG as a third + backend that needs no library. Output format selectable + (`SET BACKEND` or per-call argument). + +Each module wants its own design pass before implementation; the +sketches above are the rough shapes. + ## Known Limitations - Static caps -- 32-bit / Linux builds: 1024 variables, 256 arrays, diff --git a/include/codegen.h b/include/codegen.h index 6381d18..36244a5 100644 --- a/include/codegen.h +++ b/include/codegen.h @@ -11,6 +11,9 @@ typedef struct { bool no_gc_check; /* --no-gc-check: skip gwrt_check_line per line * (no string-pool GC, no Ctrl+Break check) */ bool fast_math; /* --fast-math: skip / by-zero checks */ + const char *main_name; /* --main-name: rename entry point from + * "main" to NAME (for cross-language link; + * NULL keeps "main") */ } codegen_opts_t; /* Generate C source from the analyzed program */ diff --git a/src/codegen.c b/src/codegen.c index 0073c59..61df750 100644 --- a/src/codegen.c +++ b/src/codegen.c @@ -21,6 +21,7 @@ static analysis_t *ana; static bool safe_mode; static bool no_gc_check; static bool fast_math; +static const char *main_name; static uint16_t emit_line; /* current BASIC line number being emitted */ static uint8_t *tp; /* token pointer (mirrors gw.text_ptr) */ static int ret_label_counter; @@ -2743,6 +2744,7 @@ void codegen_emit(FILE *f, analysis_t *a, const codegen_opts_t *opts) safe_mode = opts ? opts->safe_mode : false; no_gc_check = opts ? opts->no_gc_check : false; fast_math = opts ? opts->fast_math : false; + main_name = (opts && opts->main_name) ? opts->main_name : "main"; ret_label_counter = 0; for_label_counter = 0; for_stack_sp = 0; @@ -2787,8 +2789,9 @@ void codegen_emit(FILE *f, analysis_t *a, const codegen_opts_t *opts) EMIT("\n"); } - /* Main function */ - EMIT("int main(int argc, char **argv) {\n"); + /* Entry point. Default name is "main" for standalone executables; + * --main-name renames it for link into a larger C / Fortran project. */ + EMIT("int %s(int argc, char **argv) {\n", main_name); EMIT(" (void)argc; (void)argv;\n"); EMIT(" gwrt_init();\n"); if (a->data_count > 0) diff --git a/src/compiler_main.c b/src/compiler_main.c index 7c014de..e225df0 100644 --- a/src/compiler_main.c +++ b/src/compiler_main.c @@ -155,6 +155,9 @@ static void usage(void) " --safe=sanitize Above + address/UB sanitizers (with -c)\n" " --no-gc-check Skip per-line gwrt_check_line() (no GC, no Break)\n" " --fast-math Skip division-by-zero checks\n" + " --emit-obj Compile to object file (.o) instead of executable\n" + " --main-name N Rename emitted entry point from main to N (for\n" + " linking BASIC into a larger C/Fortran project)\n" ); } @@ -171,6 +174,8 @@ int main(int argc, char **argv) bool sanitize_mode = false; bool no_gc_check = false; bool fast_math = false; + bool emit_obj = false; + const char *main_name = NULL; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) @@ -193,6 +198,12 @@ int main(int argc, char **argv) no_gc_check = true; else if (strcmp(argv[i], "--fast-math") == 0) fast_math = true; + else if (strcmp(argv[i], "--emit-obj") == 0) + emit_obj = true; + else if (strcmp(argv[i], "--main-name") == 0 && i + 1 < argc) + main_name = argv[++i]; + else if (strncmp(argv[i], "--main-name=", 12) == 0) + main_name = argv[i] + 12; else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(); return 0; @@ -227,7 +238,7 @@ int main(int argc, char **argv) /* Code generation */ const char *c_file = output; char c_file_buf[512]; - if (compile_exe && !output) { + if ((compile_exe || emit_obj) && !output) { snprintf(c_file_buf, sizeof(c_file_buf), "%s.c", input); c_file = c_file_buf; } @@ -243,32 +254,49 @@ int main(int argc, char **argv) .warn_mode = warn_mode, .no_gc_check = no_gc_check, .fast_math = fast_math, + .main_name = main_name, }; codegen_emit(f, &analysis, &opts); if (f != stdout) fclose(f); - /* Compile to executable if requested */ - if (compile_exe) { + /* Compile to executable or object file if requested */ + if (compile_exe || emit_obj) { char cmd[2048]; const char *rt = runtime_dir ? runtime_dir : "."; - /* Derive executable name from input */ - char exe_name[512]; - strncpy(exe_name, input, sizeof(exe_name) - 1); - char *dot = strrchr(exe_name, '.'); + /* Derive output name from input (drop extension, pick .o or no + * suffix for the executable). */ + char out_name[512]; + strncpy(out_name, input, sizeof(out_name) - 1); + out_name[sizeof(out_name) - 1] = '\0'; + char *dot = strrchr(out_name, '.'); if (dot) *dot = '\0'; + if (emit_obj) { + size_t len = strlen(out_name); + if (len + 3 < sizeof(out_name)) + strcpy(out_name + len, ".o"); + } const char *san_flags = sanitize_mode ? " -fsanitize=address,undefined -fno-sanitize-recover=all" : ""; + if (emit_obj) { + /* Compile-only; the host project handles the link step. + * No -L / -l flags here -- those belong on the user's link + * command line. */ + snprintf(cmd, sizeof(cmd), + "gcc -O%d%s -c -o %s %s -I%s/include 2>&1", + opt_level, san_flags, out_name, c_file, rt); + } else { #ifdef GWRT_HAS_PULSEAUDIO - const char *pulse_lib = " -lpulse-simple"; + const char *pulse_lib = " -lpulse-simple"; #else - const char *pulse_lib = ""; + const char *pulse_lib = ""; #endif - snprintf(cmd, sizeof(cmd), - "gcc -O%d%s -o %s %s -I%s/include -L%s/build -lgwrt -lm -lpthread%s 2>&1", - opt_level, san_flags, exe_name, c_file, rt, rt, pulse_lib); + snprintf(cmd, sizeof(cmd), + "gcc -O%d%s -o %s %s -I%s/include -L%s/build -lgwrt -lm -lpthread%s 2>&1", + opt_level, san_flags, out_name, c_file, rt, rt, pulse_lib); + } int rc = system(cmd); if (rc != 0) { @@ -279,7 +307,7 @@ int main(int argc, char **argv) if (!keep_c && c_file != output) remove(c_file); - fprintf(stderr, "Compiled: %s\n", exe_name); + fprintf(stderr, "Compiled: %s\n", out_name); } return 0;