Skip to content

Commit 92272b1

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 92272b1

10 files changed

Lines changed: 164 additions & 42 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: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func discoverTests(t *testing.T) []struct{ source, expectedFile string } {
5353
}
5454

5555
func normalize(s string) string {
56+
s = strings.ReplaceAll(s, "\r\n", "\n")
57+
s = strings.ReplaceAll(s, "\r", "\n")
5658
lines := strings.Split(s, "\n")
5759
for i, line := range lines {
5860
lines[i] = strings.TrimRight(line, " ")
@@ -69,27 +71,33 @@ func isInterpreterOnly(path string) bool {
6971
return strings.HasPrefix(string(data), "\\ INTERPRETER-ONLY")
7072
}
7173

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"},
74+
// findCC returns the path to a C compiler, trying "cc" first then "gcc".
75+
func findCC() (string, error) {
76+
if cc, err := exec.LookPath("cc"); err == nil {
77+
return cc, nil
78+
}
79+
if gcc, err := exec.LookPath("gcc"); err == nil {
80+
return gcc, nil
81+
}
82+
return "", fmt.Errorf("no C compiler found (tried cc, gcc)")
7583
}
7684

7785
func TestEndToEnd(t *testing.T) {
78-
if _, err := exec.LookPath("cc"); err != nil {
86+
cc, err := findCC()
87+
if err != nil {
7988
t.Skip("C compiler not found, skipping end-to-end tests")
8089
}
8190

82-
platform := runtime.GOOS + "/" + runtime.GOARCH
91+
exeSuffix := ""
92+
if runtime.GOOS == "windows" {
93+
exeSuffix = ".exe"
94+
}
95+
8396
for _, tt := range discoverTests(t) {
8497
t.Run(filepath.Base(tt.source), func(t *testing.T) {
8598
if isInterpreterOnly(tt.source) {
8699
t.Skip("interpreter-only test")
87100
}
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-
}
93101

94102
expected, err := os.ReadFile(tt.expectedFile)
95103
if err != nil {
@@ -131,8 +139,8 @@ func TestEndToEnd(t *testing.T) {
131139
}
132140

133141
// Compile
134-
exeFile := filepath.Join(tmpDir, "output")
135-
cmd := exec.Command("cc", "-std=c11", "-O2",
142+
exeFile := filepath.Join(tmpDir, "output"+exeSuffix)
143+
cmd := exec.Command(cc, "-std=c11", "-O2",
136144
"-o", exeFile,
137145
cFile,
138146
"-lm",
@@ -160,7 +168,6 @@ func TestEndToEnd(t *testing.T) {
160168
}
161169

162170
func TestEndToEndInterpreter(t *testing.T) {
163-
platform := runtime.GOOS + "/" + runtime.GOARCH
164171
for _, tt := range discoverTests(t) {
165172
t.Run(filepath.Base(tt.source), func(t *testing.T) {
166173
// Skip benchmarks in interpreter mode — they are too slow and
@@ -169,11 +176,6 @@ func TestEndToEndInterpreter(t *testing.T) {
169176
if strings.Contains(tt.source, "benchmarks/") || base == "bench.fth" || base == "whetstone.fth" {
170177
t.Skip("benchmark file, skipping in interpreter mode")
171178
}
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-
}
177179
expected, err := os.ReadFile(tt.expectedFile)
178180
if err != nil {
179181
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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ func (e *Emitter) emitLineDirective(file string, line int) {
6767
}
6868
e.lastFile = file
6969
e.lastLine = line
70-
fmt.Fprintf(e.Buf, "#line %d \"%s\"\n", line, file)
70+
// Use forward slashes in #line paths to avoid C escape sequence issues on Windows
71+
fmt.Fprintf(e.Buf, "#line %d \"%s\"\n", line, strings.ReplaceAll(file, "\\", "/"))
7172
}
7273

7374
// resetLineState resets the #line tracking state so the next emission gets a fresh directive.
@@ -915,7 +916,7 @@ func (e *Emitter) EmitCodeWord(entry *dictionary.DictEntry) string {
915916
e.emitLineDirective(entry.SourceFile, entry.SourceLine)
916917
e.EmitLine("static inline void %s(void) {", funcName)
917918
e.Indent++
918-
// Emit each line of C code, skipping #include lines
919+
// Emit each line of C code, skipping #include lines (they are extracted to the top)
919920
for _, line := range strings.Split(entry.CCode, "\n") {
920921
trimmed := strings.TrimSpace(line)
921922
if strings.HasPrefix(trimmed, "#include") {

pkg/forth/core.fth

Lines changed: 40 additions & 6 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
@@ -316,9 +320,10 @@ VARIABLE HLD
316320
INTERPRETER [IF] [ELSE]
317321
CODE ALLOCATE
318322
cell_t _n = POP();
319-
void* _p = malloc((size_t)_n);
323+
if (_n < 0) { PUSH(0); PUSH(-1); }
324+
else { void* _p = malloc((size_t)_n);
320325
PUSH(_p ? (cell_t)(uintptr_t)_p : 0);
321-
PUSH(_p ? 0 : -1);
326+
PUSH(_p ? 0 : -1); }
322327
;CODE
323328
CODE FREE
324329
void* _p = (void*)(uintptr_t)POP();
@@ -328,9 +333,10 @@ INTERPRETER [IF] [ELSE]
328333
CODE RESIZE
329334
cell_t _n = POP();
330335
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);
336+
if (_n < 0) { PUSH((cell_t)(uintptr_t)_p); PUSH(-1); }
337+
else { void* _new = realloc(_p, (size_t)_n);
338+
PUSH(_new ? (cell_t)(uintptr_t)_new : (cell_t)(uintptr_t)_p);
339+
PUSH(_new ? 0 : -1); }
334340
;CODE
335341
[THEN]
336342

@@ -358,12 +364,18 @@ INTERPRETER [IF] [ELSE]
358364
INTERPRETER [IF] [ELSE]
359365
CODE SYSTEM
360366
#include <stdlib.h>
367+
#ifndef _WIN32
361368
#include <sys/wait.h>
369+
#endif
362370
cell_t _u = POP(); cell_t _a = POP();
363371
char _cmd[4096]; if (_u > 4095) _u = 4095;
364372
memcpy(_cmd, CADDR(_a), (size_t)_u); _cmd[_u] = 0;
365373
int _rc = system(_cmd);
374+
#ifdef _WIN32
375+
PUSH(_rc);
376+
#else
366377
PUSH(WIFEXITED(_rc) ? WEXITSTATUS(_rc) : -1);
378+
#endif
367379
;CODE
368380
[THEN]
369381

@@ -638,7 +650,11 @@ INTERPRETER [IF] [ELSE]
638650
CODE RESIZE-FILE
639651
cell_t _fid = POP(); POP(); cell_t _sz = POP();
640652
if (_fid < 0 || _fid >= MAX_FILES || !file_table[_fid]) { PUSH(-1); return; }
653+
#ifdef _WIN32
654+
PUSH(_chsize_s(_fileno(file_table[_fid]), _sz) == 0 ? 0 : -1);
655+
#else
641656
PUSH(ftruncate(fileno(file_table[_fid]), (off_t)_sz) == 0 ? 0 : -1);
657+
#endif
642658
;CODE
643659
CODE READ-LINE
644660
cell_t _fid = POP(); cell_t _max = POP(); cell_t _a = POP();
@@ -692,14 +708,24 @@ INTERPRETER [IF] [ELSE]
692708
printf("\033[2J\033[H"); fflush(stdout);
693709
;CODE
694710
CODE KEY?
711+
#ifdef _WIN32
712+
#include <conio.h>
713+
PUSH(_kbhit() ? -1 : 0);
714+
#else
695715
#include <sys/select.h>
696716
{ fd_set _fds; struct timeval _tv = {0, 0}; FD_ZERO(&_fds); FD_SET(0, &_fds);
697717
PUSH(select(1, &_fds, NULL, NULL, &_tv) > 0 ? -1 : 0); }
718+
#endif
698719
;CODE
699720
CODE MS
700-
#include <time.h>
701721
fflush(stdout);
722+
#ifdef _WIN32
723+
#include <windows.h>
724+
{ cell_t _ms = POP(); Sleep((DWORD)_ms); }
725+
#else
726+
#include <time.h>
702727
{ cell_t _ms = POP(); struct timespec _ts = { _ms / 1000, (_ms % 1000) * 1000000L }; nanosleep(&_ts, NULL); }
728+
#endif
703729
;CODE
704730
CODE TIME&DATE
705731
#include <time.h>
@@ -708,10 +734,18 @@ INTERPRETER [IF] [ELSE]
708734
PUSH(_tm->tm_mday); PUSH(_tm->tm_mon + 1); PUSH(_tm->tm_year + 1900); }
709735
;CODE
710736
CODE UTIME
737+
#ifdef _WIN32
738+
#include <windows.h>
739+
{ FILETIME _ft; GetSystemTimePreciseAsFileTime(&_ft);
740+
uint64_t _t = ((uint64_t)_ft.dwHighDateTime << 32) | _ft.dwLowDateTime;
741+
_t = _t / 10 - 11644473600000000ULL;
742+
PUSH((cell_t)_t); PUSH(0); }
743+
#else
711744
#include <sys/time.h>
712745
{ struct timeval _tv; gettimeofday(&_tv, NULL);
713746
PUSH((cell_t)((uint64_t)_tv.tv_sec * 1000000ULL + (uint64_t)_tv.tv_usec));
714747
PUSH(0); }
748+
#endif
715749
;CODE
716750
CODE XEMIT
717751
{ cell_t _xc = POP(); unsigned char _buf[4]; int _n;

0 commit comments

Comments
 (0)