Skip to content

Commit 556c5a1

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), fixing the macOS ARM test failure. Add #ifdef _WIN32 guards to POSIX-specific CODE words (KEY?, MS, UTIME, SYSTEM, SETENV, RESIZE-FILE) and runtime headers. Update codegen to keep conditional #include directives in function bodies rather than extracting them globally. Add Windows defaults for CLI (gcc, .exe), test infrastructure (exe suffix, gcc fallback), and CI (MSYS2/MinGW test job).
1 parent e5e1628 commit 556c5a1

9 files changed

Lines changed: 107 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,26 @@ 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+
install: mingw-w64-x86_64-gcc
45+
3846
- uses: actions/checkout@v4
3947

4048
- uses: actions/setup-go@v5
4149
with:
4250
go-version: "1.25"
4351

52+
- name: Run tests
53+
run: go test ./...
54+
4455
- name: Build binary
4556
run: go build -o ccforth.exe ./cmd/ccforth
4657

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: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,20 @@ 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 unconditional #include lines from CODE words, de-duplicated.
378+
// Includes inside #ifdef/#ifndef blocks are left in the function body for the preprocessor.
378379
func (cg *CodeGen) collectIncludes(codeWords []*dictionary.DictEntry) []string {
379380
seen := map[string]bool{}
380381
var result []string
381382
for _, w := range codeWords {
383+
depth := 0
382384
for _, line := range strings.Split(w.CCode, "\n") {
383385
trimmed := strings.TrimSpace(line)
384-
if strings.HasPrefix(trimmed, "#include") && !seen[trimmed] {
386+
if strings.HasPrefix(trimmed, "#ifdef") || strings.HasPrefix(trimmed, "#ifndef") {
387+
depth++
388+
} else if strings.HasPrefix(trimmed, "#endif") {
389+
depth--
390+
} else if strings.HasPrefix(trimmed, "#include") && depth == 0 && !seen[trimmed] {
385391
seen[trimmed] = true
386392
result = append(result, trimmed)
387393
}

pkg/compiler/emitter.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,10 +915,17 @@ 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 unconditional #include lines
919+
// (includes inside #ifdef/#ifndef blocks are kept for the preprocessor)
920+
ifdefDepth := 0
919921
for _, line := range strings.Split(entry.CCode, "\n") {
920922
trimmed := strings.TrimSpace(line)
921-
if strings.HasPrefix(trimmed, "#include") {
923+
if strings.HasPrefix(trimmed, "#ifdef") || strings.HasPrefix(trimmed, "#ifndef") {
924+
ifdefDepth++
925+
} else if strings.HasPrefix(trimmed, "#endif") {
926+
ifdefDepth--
927+
}
928+
if strings.HasPrefix(trimmed, "#include") && ifdefDepth == 0 {
922929
continue
923930
}
924931
e.EmitLine("%s", line)

pkg/forth/core.fth

Lines changed: 34 additions & 2 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
@@ -329,7 +333,7 @@ INTERPRETER [IF] [ELSE]
329333
cell_t _n = POP();
330334
void* _p = (void*)(uintptr_t)POP();
331335
void* _new = realloc(_p, (size_t)_n);
332-
PUSH(_new ? (cell_t)(uintptr_t)_new : 0);
336+
PUSH(_new ? (cell_t)(uintptr_t)_new : (cell_t)(uintptr_t)_p);
333337
PUSH(_new ? 0 : -1);
334338
;CODE
335339
[THEN]
@@ -358,12 +362,18 @@ INTERPRETER [IF] [ELSE]
358362
INTERPRETER [IF] [ELSE]
359363
CODE SYSTEM
360364
#include <stdlib.h>
365+
#ifndef _WIN32
361366
#include <sys/wait.h>
367+
#endif
362368
cell_t _u = POP(); cell_t _a = POP();
363369
char _cmd[4096]; if (_u > 4095) _u = 4095;
364370
memcpy(_cmd, CADDR(_a), (size_t)_u); _cmd[_u] = 0;
365371
int _rc = system(_cmd);
372+
#ifdef _WIN32
373+
PUSH(_rc);
374+
#else
366375
PUSH(WIFEXITED(_rc) ? WEXITSTATUS(_rc) : -1);
376+
#endif
367377
;CODE
368378
[THEN]
369379

@@ -638,7 +648,11 @@ INTERPRETER [IF] [ELSE]
638648
CODE RESIZE-FILE
639649
cell_t _fid = POP(); POP(); cell_t _sz = POP();
640650
if (_fid < 0 || _fid >= MAX_FILES || !file_table[_fid]) { PUSH(-1); return; }
651+
#ifdef _WIN32
652+
PUSH(_chsize_s(_fileno(file_table[_fid]), _sz) == 0 ? 0 : -1);
653+
#else
641654
PUSH(ftruncate(fileno(file_table[_fid]), (off_t)_sz) == 0 ? 0 : -1);
655+
#endif
642656
;CODE
643657
CODE READ-LINE
644658
cell_t _fid = POP(); cell_t _max = POP(); cell_t _a = POP();
@@ -692,14 +706,24 @@ INTERPRETER [IF] [ELSE]
692706
printf("\033[2J\033[H"); fflush(stdout);
693707
;CODE
694708
CODE KEY?
709+
#ifdef _WIN32
710+
#include <conio.h>
711+
PUSH(_kbhit() ? -1 : 0);
712+
#else
695713
#include <sys/select.h>
696714
{ fd_set _fds; struct timeval _tv = {0, 0}; FD_ZERO(&_fds); FD_SET(0, &_fds);
697715
PUSH(select(1, &_fds, NULL, NULL, &_tv) > 0 ? -1 : 0); }
716+
#endif
698717
;CODE
699718
CODE MS
700-
#include <time.h>
701719
fflush(stdout);
720+
#ifdef _WIN32
721+
#include <windows.h>
722+
{ cell_t _ms = POP(); Sleep((DWORD)_ms); }
723+
#else
724+
#include <time.h>
702725
{ cell_t _ms = POP(); struct timespec _ts = { _ms / 1000, (_ms % 1000) * 1000000L }; nanosleep(&_ts, NULL); }
726+
#endif
703727
;CODE
704728
CODE TIME&DATE
705729
#include <time.h>
@@ -708,10 +732,18 @@ INTERPRETER [IF] [ELSE]
708732
PUSH(_tm->tm_mday); PUSH(_tm->tm_mon + 1); PUSH(_tm->tm_year + 1900); }
709733
;CODE
710734
CODE UTIME
735+
#ifdef _WIN32
736+
#include <windows.h>
737+
{ FILETIME _ft; GetSystemTimePreciseAsFileTime(&_ft);
738+
uint64_t _t = ((uint64_t)_ft.dwHighDateTime << 32) | _ft.dwLowDateTime;
739+
_t = _t / 10 - 11644473600000000ULL;
740+
PUSH((cell_t)_t); PUSH(0); }
741+
#else
711742
#include <sys/time.h>
712743
{ struct timeval _tv; gettimeofday(&_tv, NULL);
713744
PUSH((cell_t)((uint64_t)_tv.tv_sec * 1000000ULL + (uint64_t)_tv.tv_usec));
714745
PUSH(0); }
746+
#endif
715747
;CODE
716748
CODE XEMIT
717749
{ 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;

testdata/end-to-end/forth2012/memory_alloc.fth

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,7 @@ T{ ADDR1 @ 200 CHARS RESIZE SWAP ADDR1 ! -> 0 }T
5959
T{ ADDR1 @ 28 CHECKMEM -> TRUE }T
6060

6161
\ Failure of RESIZE and ALLOCATE
62-
[ INTERPRETER ] [IF]
63-
\ Compiled RESIZE doesn't preserve original addr on failure
6462
T{ ADDR1 @ -1 CHARS RESIZE 0= -> ADDR1 @ FALSE }T
65-
[THEN]
6663

6764
T{ ADDR1 @ FREE -> 0 }T
6865

0 commit comments

Comments
 (0)