Skip to content

Commit 257b126

Browse files
committed
Fix macOS ARM RESIZE test failure and add Windows platform support
Fix compiled RESIZE to return original address on failure (Forth 2012 spec), with early return for negative sizes to avoid platform-specific realloc edge cases. Add #ifdef _WIN32 guards to POSIX-specific CODE words (KEY?, MS, UTIME, SYSTEM, SETENV, RESIZE-FILE) and runtime headers. Update codegen to extract conditional #include directives to the top level with proper #ifdef guards. Add Windows defaults for CLI (gcc, .exe), test infrastructure (exe suffix, gcc fallback), and CI (MSYS2/MinGW test job with PATH inheritance).
1 parent e5e1628 commit 257b126

9 files changed

Lines changed: 179 additions & 57 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,27 @@ jobs:
3232
name: ccforth-${{ matrix.os }}
3333
path: ccforth
3434

35-
build-windows:
35+
test-windows:
3636
runs-on: windows-latest
37+
defaults:
38+
run:
39+
shell: msys2 {0}
3740
steps:
41+
- uses: msys2/setup-msys2@v2
42+
with:
43+
msystem: MINGW64
44+
path-type: inherit
45+
install: mingw-w64-x86_64-gcc
46+
3847
- uses: actions/checkout@v4
3948

4049
- uses: actions/setup-go@v5
4150
with:
4251
go-version: "1.25"
4352

53+
- name: Run tests
54+
run: go test ./...
55+
4456
- name: Build binary
4557
run: go build -o ccforth.exe ./cmd/ccforth
4658

bench_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"runtime"
910
"sort"
1011
"strings"
1112
"testing"
@@ -59,8 +60,16 @@ func compileForBench(b *testing.B, fthFile string) string {
5960
b.Fatalf("write C error: %v", err)
6061
}
6162

62-
exeFile := filepath.Join(tmpDir, "bench")
63-
cmd := exec.Command("cc", "-std=c11", "-O3", "-march=native", "-flto", "-o", exeFile, cFile, "-lm")
63+
exeSuffix := ""
64+
if runtime.GOOS == "windows" {
65+
exeSuffix = ".exe"
66+
}
67+
exeFile := filepath.Join(tmpDir, "bench"+exeSuffix)
68+
cc, err := findCC()
69+
if err != nil {
70+
b.Skip("C compiler not found")
71+
}
72+
cmd := exec.Command(cc, "-std=c11", "-O3", "-march=native", "-flto", "-o", exeFile, cFile, "-lm")
6473
var compileErr bytes.Buffer
6574
cmd.Stderr = &compileErr
6675
if err := cmd.Run(); err != nil {
@@ -97,7 +106,7 @@ func loadForInterpreter(b *testing.B, fthFile string) *interpreter.Interpreter {
97106
// BenchmarkCompiled benchmarks compiled (.fth -> C -> native) execution.
98107
// Each sub-benchmark compiles once during setup, then runs the binary b.N times.
99108
func BenchmarkCompiled(b *testing.B) {
100-
if _, err := exec.LookPath("cc"); err != nil {
109+
if _, err := findCC(); err != nil {
101110
b.Skip("C compiler not found")
102111
}
103112

@@ -178,7 +187,7 @@ func BenchmarkGforth(b *testing.B) {
178187

179188
// BenchmarkFib benchmarks the Fibonacci benchmark compiled.
180189
func BenchmarkFib(b *testing.B) {
181-
if _, err := exec.LookPath("cc"); err != nil {
190+
if _, err := findCC(); err != nil {
182191
b.Skip("C compiler not found")
183192
}
184193
exeFile := compileForBench(b, "testdata/end-to-end/bench.fth")

cmd/ccforth/main.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"runtime"
910
"strings"
1011

1112
"github.com/chzyer/readline"
@@ -15,9 +16,15 @@ import (
1516
)
1617

1718
func main() {
18-
outputPath := flag.String("o", "a.out", "output executable path")
19+
defaultOutput := "a.out"
20+
defaultCC := "cc"
21+
if runtime.GOOS == "windows" {
22+
defaultOutput = "a.exe"
23+
defaultCC = "gcc"
24+
}
25+
outputPath := flag.String("o", defaultOutput, "output executable path")
1926
emitCOnly := flag.Bool("c", false, "emit C only, don't invoke compiler")
20-
ccCompiler := flag.String("cc", "cc", "C compiler to use")
27+
ccCompiler := flag.String("cc", defaultCC, "C compiler to use")
2128
cFlags := flag.String("Cflags", "", "extra flags for C compiler")
2229
entryWord := flag.String("entry", "MAIN", "entry word")
2330
memSize := flag.Int("memsize", 1048576, "FORTH memory size in bytes")

integration_test.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,33 @@ func isInterpreterOnly(path string) bool {
6969
return strings.HasPrefix(string(data), "\\ INTERPRETER-ONLY")
7070
}
7171

72-
// knownFailures lists tests that fail on specific GOOS/GOARCH combinations.
73-
var knownFailures = map[string]map[string]string{
74-
"memory_alloc.fth": {"darwin/arm64": "RESIZE behaviour differs on macOS ARM"},
72+
// findCC returns the path to a C compiler, trying "cc" first then "gcc".
73+
func findCC() (string, error) {
74+
if cc, err := exec.LookPath("cc"); err == nil {
75+
return cc, nil
76+
}
77+
if gcc, err := exec.LookPath("gcc"); err == nil {
78+
return gcc, nil
79+
}
80+
return "", fmt.Errorf("no C compiler found (tried cc, gcc)")
7581
}
7682

7783
func TestEndToEnd(t *testing.T) {
78-
if _, err := exec.LookPath("cc"); err != nil {
84+
cc, err := findCC()
85+
if err != nil {
7986
t.Skip("C compiler not found, skipping end-to-end tests")
8087
}
8188

82-
platform := runtime.GOOS + "/" + runtime.GOARCH
89+
exeSuffix := ""
90+
if runtime.GOOS == "windows" {
91+
exeSuffix = ".exe"
92+
}
93+
8394
for _, tt := range discoverTests(t) {
8495
t.Run(filepath.Base(tt.source), func(t *testing.T) {
8596
if isInterpreterOnly(tt.source) {
8697
t.Skip("interpreter-only test")
8798
}
88-
if reasons, ok := knownFailures[filepath.Base(tt.source)]; ok {
89-
if reason, ok := reasons[platform]; ok {
90-
t.Skipf("known failure on %s: %s", platform, reason)
91-
}
92-
}
9399

94100
expected, err := os.ReadFile(tt.expectedFile)
95101
if err != nil {
@@ -131,8 +137,8 @@ func TestEndToEnd(t *testing.T) {
131137
}
132138

133139
// Compile
134-
exeFile := filepath.Join(tmpDir, "output")
135-
cmd := exec.Command("cc", "-std=c11", "-O2",
140+
exeFile := filepath.Join(tmpDir, "output"+exeSuffix)
141+
cmd := exec.Command(cc, "-std=c11", "-O2",
136142
"-o", exeFile,
137143
cFile,
138144
"-lm",
@@ -160,7 +166,6 @@ func TestEndToEnd(t *testing.T) {
160166
}
161167

162168
func TestEndToEndInterpreter(t *testing.T) {
163-
platform := runtime.GOOS + "/" + runtime.GOARCH
164169
for _, tt := range discoverTests(t) {
165170
t.Run(filepath.Base(tt.source), func(t *testing.T) {
166171
// Skip benchmarks in interpreter mode — they are too slow and
@@ -169,11 +174,6 @@ func TestEndToEndInterpreter(t *testing.T) {
169174
if strings.Contains(tt.source, "benchmarks/") || base == "bench.fth" || base == "whetstone.fth" {
170175
t.Skip("benchmark file, skipping in interpreter mode")
171176
}
172-
if reasons, ok := knownFailures[base]; ok {
173-
if reason, ok := reasons[platform]; ok {
174-
t.Skipf("known failure on %s: %s", platform, reason)
175-
}
176-
}
177177
expected, err := os.ReadFile(tt.expectedFile)
178178
if err != nil {
179179
t.Fatalf("read expected: %v", err)

pkg/codegen/codegen.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -374,19 +374,70 @@ func (cg *CodeGen) emitValueInit(buf *bytes.Buffer, values []*dictionary.DictEnt
374374
}
375375
}
376376

377-
// collectIncludes extracts #include lines from CODE words, de-duplicated.
377+
// collectIncludes extracts #include lines from CODE words, preserving #ifdef guards.
378+
// Includes are grouped by their preprocessor guard context and de-duplicated.
378379
func (cg *CodeGen) collectIncludes(codeWords []*dictionary.DictEntry) []string {
379-
seen := map[string]bool{}
380-
var result []string
380+
type guardedInclude struct {
381+
guard string // "" for unconditional, or e.g. "#ifdef _WIN32", "#ifndef _WIN32"
382+
include string
383+
}
384+
seen := map[string]bool{} // "guard|include" dedup key
385+
var includes []guardedInclude
386+
381387
for _, w := range codeWords {
388+
var guardStack []string
382389
for _, line := range strings.Split(w.CCode, "\n") {
383390
trimmed := strings.TrimSpace(line)
384-
if strings.HasPrefix(trimmed, "#include") && !seen[trimmed] {
385-
seen[trimmed] = true
386-
result = append(result, trimmed)
391+
if strings.HasPrefix(trimmed, "#ifdef") || strings.HasPrefix(trimmed, "#ifndef") {
392+
guardStack = append(guardStack, trimmed)
393+
} else if strings.HasPrefix(trimmed, "#else") {
394+
if len(guardStack) > 0 {
395+
last := guardStack[len(guardStack)-1]
396+
if strings.HasPrefix(last, "#ifdef") {
397+
guardStack[len(guardStack)-1] = "#ifndef" + last[len("#ifdef"):]
398+
} else if strings.HasPrefix(last, "#ifndef") {
399+
guardStack[len(guardStack)-1] = "#ifdef" + last[len("#ifndef"):]
400+
}
401+
}
402+
} else if strings.HasPrefix(trimmed, "#endif") {
403+
if len(guardStack) > 0 {
404+
guardStack = guardStack[:len(guardStack)-1]
405+
}
406+
} else if strings.HasPrefix(trimmed, "#include") {
407+
guard := ""
408+
if len(guardStack) > 0 {
409+
guard = guardStack[len(guardStack)-1]
410+
}
411+
key := guard + "|" + trimmed
412+
if !seen[key] {
413+
seen[key] = true
414+
includes = append(includes, guardedInclude{guard: guard, include: trimmed})
415+
}
387416
}
388417
}
389418
}
419+
420+
// Group by guard for cleaner output
421+
groups := map[string][]string{} // guard -> includes
422+
var guardOrder []string
423+
for _, gi := range includes {
424+
if _, ok := groups[gi.guard]; !ok {
425+
guardOrder = append(guardOrder, gi.guard)
426+
}
427+
groups[gi.guard] = append(groups[gi.guard], gi.include)
428+
}
429+
430+
var result []string
431+
for _, guard := range guardOrder {
432+
incs := groups[guard]
433+
if guard == "" {
434+
result = append(result, incs...)
435+
} else {
436+
result = append(result, guard)
437+
result = append(result, incs...)
438+
result = append(result, "#endif")
439+
}
440+
}
390441
return result
391442
}
392443

pkg/compiler/emitter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ func (e *Emitter) EmitCodeWord(entry *dictionary.DictEntry) string {
915915
e.emitLineDirective(entry.SourceFile, entry.SourceLine)
916916
e.EmitLine("static inline void %s(void) {", funcName)
917917
e.Indent++
918-
// Emit each line of C code, skipping #include lines
918+
// Emit each line of C code, skipping #include lines (they are extracted to the top)
919919
for _, line := range strings.Split(entry.CCode, "\n") {
920920
trimmed := strings.TrimSpace(line)
921921
if strings.HasPrefix(trimmed, "#include") {

pkg/forth/core.fth

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,11 @@ INTERPRETER [IF] [ELSE]
228228
cell_t _vu = POP(); unsigned char *_vp = CADDR(POP());
229229
unsigned char _ns = _np[_nu]; _np[_nu] = 0;
230230
unsigned char _vs = _vp[_vu]; _vp[_vu] = 0;
231+
#ifdef _WIN32
232+
cell_t _r = _putenv_s((char*)_np, (char*)_vp);
233+
#else
231234
cell_t _r = setenv((char*)_np, (char*)_vp, 1);
235+
#endif
232236
_np[_nu] = _ns; _vp[_vu] = _vs;
233237
PUSH(_r); }
234238
;CODE
@@ -328,9 +332,10 @@ INTERPRETER [IF] [ELSE]
328332
CODE RESIZE
329333
cell_t _n = POP();
330334
void* _p = (void*)(uintptr_t)POP();
331-
void* _new = realloc(_p, (size_t)_n);
332-
PUSH(_new ? (cell_t)(uintptr_t)_new : 0);
333-
PUSH(_new ? 0 : -1);
335+
if (_n < 0) { PUSH((cell_t)(uintptr_t)_p); PUSH(-1); }
336+
else { void* _new = realloc(_p, (size_t)_n);
337+
PUSH(_new ? (cell_t)(uintptr_t)_new : (cell_t)(uintptr_t)_p);
338+
PUSH(_new ? 0 : -1); }
334339
;CODE
335340
[THEN]
336341

@@ -358,12 +363,18 @@ INTERPRETER [IF] [ELSE]
358363
INTERPRETER [IF] [ELSE]
359364
CODE SYSTEM
360365
#include <stdlib.h>
366+
#ifndef _WIN32
361367
#include <sys/wait.h>
368+
#endif
362369
cell_t _u = POP(); cell_t _a = POP();
363370
char _cmd[4096]; if (_u > 4095) _u = 4095;
364371
memcpy(_cmd, CADDR(_a), (size_t)_u); _cmd[_u] = 0;
365372
int _rc = system(_cmd);
373+
#ifdef _WIN32
374+
PUSH(_rc);
375+
#else
366376
PUSH(WIFEXITED(_rc) ? WEXITSTATUS(_rc) : -1);
377+
#endif
367378
;CODE
368379
[THEN]
369380

@@ -638,7 +649,11 @@ INTERPRETER [IF] [ELSE]
638649
CODE RESIZE-FILE
639650
cell_t _fid = POP(); POP(); cell_t _sz = POP();
640651
if (_fid < 0 || _fid >= MAX_FILES || !file_table[_fid]) { PUSH(-1); return; }
652+
#ifdef _WIN32
653+
PUSH(_chsize_s(_fileno(file_table[_fid]), _sz) == 0 ? 0 : -1);
654+
#else
641655
PUSH(ftruncate(fileno(file_table[_fid]), (off_t)_sz) == 0 ? 0 : -1);
656+
#endif
642657
;CODE
643658
CODE READ-LINE
644659
cell_t _fid = POP(); cell_t _max = POP(); cell_t _a = POP();
@@ -692,14 +707,24 @@ INTERPRETER [IF] [ELSE]
692707
printf("\033[2J\033[H"); fflush(stdout);
693708
;CODE
694709
CODE KEY?
710+
#ifdef _WIN32
711+
#include <conio.h>
712+
PUSH(_kbhit() ? -1 : 0);
713+
#else
695714
#include <sys/select.h>
696715
{ fd_set _fds; struct timeval _tv = {0, 0}; FD_ZERO(&_fds); FD_SET(0, &_fds);
697716
PUSH(select(1, &_fds, NULL, NULL, &_tv) > 0 ? -1 : 0); }
717+
#endif
698718
;CODE
699719
CODE MS
700-
#include <time.h>
701720
fflush(stdout);
721+
#ifdef _WIN32
722+
#include <windows.h>
723+
{ cell_t _ms = POP(); Sleep((DWORD)_ms); }
724+
#else
725+
#include <time.h>
702726
{ cell_t _ms = POP(); struct timespec _ts = { _ms / 1000, (_ms % 1000) * 1000000L }; nanosleep(&_ts, NULL); }
727+
#endif
703728
;CODE
704729
CODE TIME&DATE
705730
#include <time.h>
@@ -708,10 +733,18 @@ INTERPRETER [IF] [ELSE]
708733
PUSH(_tm->tm_mday); PUSH(_tm->tm_mon + 1); PUSH(_tm->tm_year + 1900); }
709734
;CODE
710735
CODE UTIME
736+
#ifdef _WIN32
737+
#include <windows.h>
738+
{ FILETIME _ft; GetSystemTimePreciseAsFileTime(&_ft);
739+
uint64_t _t = ((uint64_t)_ft.dwHighDateTime << 32) | _ft.dwLowDateTime;
740+
_t = _t / 10 - 11644473600000000ULL;
741+
PUSH((cell_t)_t); PUSH(0); }
742+
#else
711743
#include <sys/time.h>
712744
{ struct timeval _tv; gettimeofday(&_tv, NULL);
713745
PUSH((cell_t)((uint64_t)_tv.tv_sec * 1000000ULL + (uint64_t)_tv.tv_usec));
714746
PUSH(0); }
747+
#endif
715748
;CODE
716749
CODE XEMIT
717750
{ cell_t _xc = POP(); unsigned char _buf[4]; int _n;

runtime/runtime.c.src

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
#ifndef _WIN32
12
#define _POSIX_C_SOURCE 200809L
3+
#endif
24

35
#include <stdint.h>
46
#include <stdio.h>
57
#include <stdlib.h>
68
#include <string.h>
79
#include <setjmp.h>
10+
#ifndef _WIN32
811
#include <unistd.h>
12+
#endif
913
#include <math.h>
1014

1115
typedef int64_t cell_t;

0 commit comments

Comments
 (0)