Log in
Makefile 607 lines · 25.3 KB · makefile Blame
1
# Spinel AOT Compiler - Makefile
2
#
3
# Usage:
4
#   make              Build the C compiler (runtime + spinel + tools)
5
#   make test         Run the feature tests (always a fresh run)
6
#   make bench        Run benchmarks vs CRuby
7
#   make optcarrot    End-to-end optcarrot integration test
8
#   make legacy       Build the legacy Ruby compiler (delegates to legacy/)
9
#   make bootstrap    Legacy self-host fixpoint check (delegates to legacy/)
10
#   make check        Fast pre-commit: rebuild + tests
11
#   make gate         Full pre-push: test || bench || optcarrot
12
#   make clean        Remove built binaries
13
#
14
# The legacy Ruby compiler (legacy/) is built ONLY by the explicit `make legacy`
15
# / `make bootstrap` delegators or by `cd legacy && make`; the C compiler is
16
# master and a plain `make` / `make gate` never compiles it.
17
18
# Shared toolchain configuration (CC wrapping, CFLAGS, LTO, stamps, …).
19
include common.mk
20
21
# Prism library: prefer vendor/prism (fetched via `make deps`), then fall
22
# back to the Prism gem if one is installed. Override with PRISM_DIR=…
23
PRISM_VERSION ?= 1.9.0
24
ifneq ($(wildcard vendor/prism/include/prism.h),)
25
  PRISM_DIR ?= vendor/prism
26
else
27
  PRISM_DIR ?= $(shell ruby -rprism -e 'puts $$LOADED_FEATURES.grep(/prism/).first.sub(%r{/lib/.*}, "")' 2>/dev/null)
28
endif
29
30
PRISM_INC    = $(PRISM_DIR)/include
31
PRISM_SRC    = $(wildcard $(PRISM_DIR)/src/*.c) $(wildcard $(PRISM_DIR)/src/util/*.c)
32
PRISM_OBJ    = $(patsubst $(PRISM_DIR)/src/%.c,build/prism/%.o,$(PRISM_SRC))
33
PRISM_LIB    = build/libprism.a
34
35
# rbs C parser. Fetched via `make deps` from rubygems.org. Consumed by
36
# spinel_rbs_extract to produce a seed file for the analyzer.
37
RBS_VERSION ?= 4.0.1
38
RBS_DIR      = vendor/rbs
39
RBS_INC      = $(RBS_DIR)/include
40
RBS_SRC      = $(wildcard $(RBS_DIR)/src/*.c) $(wildcard $(RBS_DIR)/src/util/*.c)
41
RBS_OBJ      = $(patsubst $(RBS_DIR)/src/%.c,build/rbs/%.o,$(RBS_SRC))
42
RBS_LIB      = build/librbs.a
43
44
.PHONY: all parse legacy bootstrap codegen regexp rbs_extract rbs-test rbs-seed-test \
45
        analyze-fail-test test test-run clean-test-results regen-rbs-expected \
46
        regen-expected bench optcarrot gate check gate-legs gate-test gate-bench \
47
        gate-optcarrot clean install uninstall deps tools
48
49
# `make all` includes the RBS extractor when vendor/rbs has been fetched
50
# (via `make deps`); without it the extractor is silently omitted. Built under
51
# build/ like other intermediates; rbs-seed-test copies it beside $(SPINEL),
52
# where main.c looks for it as a sibling at runtime.
53
RBS_EXTRACT_BIN = build/spinel_rbs_extract
54
ifneq ($(wildcard $(RBS_INC)/rbs/parser.h),)
55
  RBS_EXTRACT_TARGET = $(RBS_EXTRACT_BIN)
56
else
57
  RBS_EXTRACT_TARGET =
58
endif
59
60
# The single Spinel binary (compiler + cc driver). Defined here, before the
61
# `all` rule, because a rule's prerequisites are expanded as it is read.
62
# Built into bin/ alongside the companion tools (spinel-doctor, ...); bin/ sits
63
# beside lib/ so the binary resolves its runtime lib via ../lib, same as before.
64
SPINEL = bin/spinel
65
66
all: regexp $(SPINEL) $(RBS_EXTRACT_TARGET) tools
67
68
# ---- Dependencies ----
69
deps: vendor/prism/include/prism/diagnostic.h vendor/rbs/include/rbs/parser.h
70
71
# Download the pre-built Prism gem from rubygems.org and extract its C
72
# sources (the .gem ships the generated headers β€” no rake/bundler needed).
73
vendor/prism/include/prism/diagnostic.h:
74
	@mkdir -p vendor/prism
75
	@echo "Fetching prism v$(PRISM_VERSION) from rubygems.org..."
76
	curl -sL -o /tmp/prism-$(PRISM_VERSION).gem https://rubygems.org/gems/prism-$(PRISM_VERSION).gem
77
	@tmpdir=$$(mktemp -d); \
78
	 tar -xf /tmp/prism-$(PRISM_VERSION).gem -C $$tmpdir data.tar.gz; \
79
	 tar -xzf $$tmpdir/data.tar.gz -C vendor/prism; \
80
	 rm -rf $$tmpdir /tmp/prism-$(PRISM_VERSION).gem
81
	@test -f $@ && echo "prism v$(PRISM_VERSION) ready at vendor/prism"
82
83
# Same shape: download the rbs gem and extract its bundled C parser.
84
vendor/rbs/include/rbs/parser.h:
85
	@mkdir -p vendor/rbs
86
	@echo "Fetching rbs v$(RBS_VERSION) from rubygems.org..."
87
	curl -sL -o /tmp/rbs-$(RBS_VERSION).gem https://rubygems.org/gems/rbs-$(RBS_VERSION).gem
88
	@tmpdir=$$(mktemp -d); \
89
	 tar -xf /tmp/rbs-$(RBS_VERSION).gem -C $$tmpdir data.tar.gz; \
90
	 tar -xzf $$tmpdir/data.tar.gz -C vendor/rbs; \
91
	 rm -rf $$tmpdir /tmp/rbs-$(RBS_VERSION).gem
92
	@test -f $@ && echo "rbs v$(RBS_VERSION) ready at vendor/rbs"
93
94
# If PRISM_DIR ended up empty (no vendor/prism, no gem), halt with a clear
95
# message before trying to build anything that needs it.
96
ifeq ($(PRISM_DIR),)
97
parse codegen regexp all: prism-missing
98
prism-missing:
99
	@echo "Error: Prism not found."; \
100
	 echo "  Run 'make deps' to fetch libprism into vendor/prism,"; \
101
	 echo "  or install the prism gem (gem install prism),"; \
102
	 echo "  or set PRISM_DIR=/path/to/prism manually."; \
103
	 exit 1
104
endif
105
106
# ---- Prism static library ----
107
108
build/libprism.a: $(PRISM_OBJ)
109
	@mkdir -p build
110
	ar rcs $@ $^
111
112
build/prism/%.o: $(PRISM_DIR)/src/%.c
113
	@mkdir -p $(dir $@)
114
	$(CC) -c -O2 -I$(PRISM_INC) -I$(PRISM_DIR)/src $< -o $@
115
116
# ---- rbs static library ----
117
118
build/librbs.a: $(RBS_OBJ)
119
	@mkdir -p build
120
	ar rcs $@ $^
121
122
build/rbs/%.o: $(RBS_DIR)/src/%.c
123
	@mkdir -p $(dir $@)
124
	$(CC) -c -O2 -Wno-all -I$(RBS_INC) -I$(RBS_DIR)/src $< -o $@
125
126
# ---- C Parser ----
127
128
parse: legacy/spinel_parse
129
130
legacy/spinel_parse: legacy/spinel_parse.c $(PRISM_LIB)
131
	$(CC) $(CFLAGS) -I$(PRISM_INC) legacy/spinel_parse.c $(PRISM_LIB) -lm -o $@
132
133
# ---- C compiler (src/) ----
134
# The single-binary C reimplementation of the analyzer + code generator.
135
# Links src/spinel_parse.c, the library copy of the Prism walk (no main();
136
# exposes sp_parse_file_to_text). The standalone-with-main copy used by the
137
# legacy `parse` target lives in legacy/spinel_parse.c.
138
# `spinel` is the single binary: it emits C and then drives cc to link it.
139
# (SPINEL itself is defined above, just before the `all` target.)
140
141
SPINEL_HDRS = src/node_table.h src/codegen.h src/codegen_internal.h src/types.h src/compiler.h src/analyze.h src/analyze_internal.h
142
SPINEL_OBJ  = build/csrc/node_table.o build/csrc/types.o build/csrc/compiler.o \
143
               build/csrc/analyze.o build/csrc/analyze_util.o build/csrc/analyze_infer.o \
144
               build/csrc/analyze_scope.o build/csrc/analyze_pass.o build/csrc/codegen.o build/csrc/codegen_util.o \
145
               build/csrc/codegen_fold.o build/csrc/codegen_call.o \
146
               build/csrc/codegen_expr.o build/csrc/codegen_stmt.o build/csrc/main.o
147
148
build/csrc:
149
	@mkdir -p build/csrc
150
151
build/csrc/%.o: src/%.c $(SPINEL_HDRS) | build/csrc
152
	$(CC) $(CFLAGS) -Isrc -c $< -o $@
153
154
build/csrc/sp_parse_lib.o: src/spinel_parse.c $(PRISM_LIB) | build/csrc
155
	$(CC) $(CFLAGS) -I$(PRISM_INC) -c src/spinel_parse.c -o $@
156
157
$(SPINEL): $(SPINEL_OBJ) build/csrc/sp_parse_lib.o $(PRISM_LIB)
158
	@mkdir -p bin
159
	$(CC) $(CFLAGS) $(SPINEL_OBJ) build/csrc/sp_parse_lib.o $(PRISM_LIB) -lm $(LDFLAGS) -o $@
160
	@# Dev convenience: a repo-root `./spinel` pointing at the built binary
161
	@# (the installed command is `spinel` too). Best-effort; gitignored.
162
	@ln -sf $@ spinel 2>/dev/null || cp $@ spinel 2>/dev/null || true
163
164
# ---- Legacy Ruby compiler (regression oracle) ----
165
# Lives in legacy/ with its own Makefile; these are thin delegators.
166
legacy:
167
	+@$(MAKE) -C legacy --no-print-directory legacy
168
169
bootstrap:
170
	+@$(MAKE) -C legacy --no-print-directory LTO=$(LTO) bootstrap
171
172
codegen: legacy
173
174
# ---- RBS extractor ----
175
# Reads sig/**/*.rbs, emits the seed-file format spinel_analyze consumes
176
# when invoked with `spinel --rbs DIR`.
177
178
ifeq ($(wildcard $(RBS_INC)/rbs/parser.h),)
179
rbs_extract: rbs-missing
180
rbs-missing:
181
	@echo "Error: rbs C parser not found at $(RBS_INC)/rbs/parser.h."; \
182
	 echo "  Run 'make deps' to fetch it from rubygems.org into vendor/rbs."; \
183
	 exit 1
184
else
185
rbs_extract: $(RBS_EXTRACT_BIN)
186
187
$(RBS_EXTRACT_BIN): tools/spinel_rbs_extract.c $(RBS_LIB)
188
	@mkdir -p $(@D)
189
	$(CC) $(CFLAGS) -I$(RBS_INC) tools/spinel_rbs_extract.c $(RBS_LIB) -o $@
190
endif
191
192
# ---- Runtime library (regexp + bigint + …) ----
193
194
RE_SRC = lib/regexp/re_compile.c lib/regexp/re_exec.c lib/regexp/re_utf8.c
195
RE_OBJ = $(patsubst lib/regexp/%.c,build/regexp/%.o,$(RE_SRC))
196
197
build/regexp/%.o: lib/regexp/%.c lib/regexp/re_internal.h
198
	@mkdir -p build/regexp
199
	$(CC) -c -O2 $(SEC_FLAGS) -Ilib/regexp $< -o $@
200
201
build/sp_bigint.o: lib/sp_bigint.c lib/sp_bigint.h lib/mruby_shim.h
202
	@mkdir -p build
203
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_bigint.c -o build/sp_bigint.o
204
205
build/sp_crypto.o: lib/sp_crypto.c lib/sp_crypto.h
206
	@mkdir -p build
207
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_crypto.c -o build/sp_crypto.o
208
209
build/sp_pack.o: lib/sp_pack.c lib/mruby_shim.h
210
	@mkdir -p build
211
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_pack.c -o build/sp_pack.o
212
213
build/sp_strscan.o: lib/sp_strscan.c lib/mruby_shim.h
214
	@mkdir -p build
215
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib -Ilib/regexp lib/sp_strscan.c -o build/sp_strscan.o
216
217
build/sp_time.o: lib/sp_time.c lib/sp_time.h
218
	@mkdir -p build
219
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_time.c -o build/sp_time.o
220
221
build/sp_core.o: lib/sp_core.c lib/sp_core.h
222
	@mkdir -p build
223
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_core.c -o build/sp_core.o
224
225
build/sp_system.o: lib/sp_system.c lib/sp_system.h
226
	@mkdir -p build
227
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_system.c -o build/sp_system.o
228
229
build/sp_gc.o: lib/sp_gc.c lib/sp_gc.h lib/sp_types.h
230
	@mkdir -p build
231
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_gc.c -o build/sp_gc.o
232
233
build/sp_fiber.o: lib/sp_fiber.c lib/sp_fiber.h lib/sp_gc.h lib/sp_types.h
234
	@mkdir -p build
235
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_fiber.c -o build/sp_fiber.o
236
237
build/sp_net.o: lib/sp_net.c lib/sp_net.h
238
	@mkdir -p build
239
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_net.c -o build/sp_net.o
240
241
build/sp_io.o: lib/sp_io.c lib/sp_io.h lib/sp_gc.h lib/sp_types.h
242
	@mkdir -p build
243
	$(CC) -c -O2 -Wno-all $(SEC_FLAGS) -Ilib lib/sp_io.c -o build/sp_io.o
244
245
SP_RT_LIB = lib/libspinel_rt.a
246
247
$(SP_RT_LIB): $(RE_OBJ) build/sp_bigint.o build/sp_crypto.o build/sp_pack.o build/sp_strscan.o build/sp_time.o build/sp_core.o build/sp_net.o build/sp_system.o build/sp_gc.o build/sp_fiber.o build/sp_io.o
248
	ar rcs $@ $^
249
250
regexp: $(SP_RT_LIB)
251
252
# ---- In-tree developer tools ----
253
254
# spinel-doctor / spinel-reduce / spinel-flatten: written in the spinel subset
255
# and compiled by spinel itself (dogfood), so their only runtime dependency is
256
# cc β€” the same as the compiler. Each tools/<name>.rb becomes bin/spinel-<name>,
257
# beside the compiler, so the `spinel-<name>` command is found next to `spinel`.
258
# A tool that no longer fits the subset breaks the build, which keeps them honest.
259
TOOL_NAMES = doctor reduce flatten
260
TOOL_BINS  = $(addprefix bin/spinel-,$(TOOL_NAMES))
261
262
tools: $(TOOL_BINS)
263
264
bin/spinel-%: tools/%.rb tools/tool_common.rb $(SPINEL) $(SP_RT_LIB)
265
	@mkdir -p bin
266
	$(SPINEL) $< -o $@
267
268
# ---- Test ----
269
270
TESTS := $(wildcard test/*.rb)
271
# Mode-incompatible: int_overflow_raises pins raise-mode semantics; under
272
# --int-overflow=promote the same code auto-promotes and output diverges.
273
ifeq ($(SPINEL_INT_OVERFLOW),promote)
274
TESTS := $(filter-out test/int_overflow_raises.rb,$(TESTS))
275
# Drive the spinel front-end and the C compile in promote mode so the test
276
# rule actually exercises the auto-promotion path end to end.
277
SP_OV_FLAG := --int-overflow=promote
278
SP_OV_DEFINE := -DSP_INT_OVERFLOW_MODE_PROMOTE
279
else
280
# `promote_*` tests overflow on purpose and only have defined output under
281
# --int-overflow=promote; in raise/wrap mode they would (correctly) raise.
282
TESTS := $(filter-out test/promote_%.rb,$(TESTS))
283
endif
284
TEST_TARGETS := $(patsubst test/%.rb,build/test-results/%.ok,$(TESTS))
285
286
# Warnings the generated-C -Werror check should not gate on. clang enables
287
# -Wunused-value by default (gcc only under -Wall, which the build disables),
288
# so a discarded value-producing statement-expression -- e.g. the
289
# `({ ...; v; })` emitted for `Fiber[:k] = v` in statement position -- fails
290
# CI under clang while gcc is silent. The value is intentionally discarded;
291
# behaviour is still gated by the output diff. Keep this list minimal.
292
TEST_WARN_SUPPRESS := -Wno-unused-value
293
294
# `make test` always runs fresh: it wipes the prior `.ok` stamps first,
295
# then runs the suite. (The old incremental `test` + `retest` split is
296
# gone β€” a stale `.ok` reading PASS was a recurring foot-gun.)
297
test:
298
	+@$(MAKE) --no-print-directory clean-test-results
299
	+@$(MAKE) --no-print-directory test-run
300
301
# The actual run. rbs-test golden-checks the RBS extractor (cheap, C-only).
302
# rbs-seed-test checks the seeds actually reach the analyzer (incl. nested
303
# classes, #1417). analyze-fail (legacy-analyzer diagnostics) lives in legacy/
304
# and runs only via an explicit `make analyze-fail-test` / `cd legacy && make`,
305
# not as part of `make gate` or the hot `make test` loop.
306
test-run: rbs-test rbs-seed-test $(TEST_TARGETS)
307
	@if [ -z "$(TIMEOUT_BIN)" ]; then echo "Note: no 'timeout' command found; running without time limits."; fi
308
	@if [ -t 1 ]; then printf '\n'; fi
309
	@pass=$$(grep -l '^PASS' build/test-results/*.ok 2>/dev/null | wc -l); \
310
	fail=$$(grep -l '^FAIL' build/test-results/*.ok 2>/dev/null | wc -l); \
311
	err=$$(grep -l '^ERR' build/test-results/*.ok 2>/dev/null | wc -l); \
312
	for f in build/test-results/*.ok; do \
313
	  bn=$$(basename "$$f" .ok); \
314
	  status=$$(cat "$$f"); \
315
	  if [ "$$status" = FAIL ]; then \
316
	    echo "FAIL: $$bn"; \
317
	    head -40 "$$f.diff"; \
318
	  elif [ "$$status" = ERR ]; then \
319
	    echo "ERR:  $$bn"; \
320
	  fi; \
321
	done; \
322
	echo "Tests: $$pass pass, $$fail fail, $$err error"; \
323
	if [ $$fail -ne 0 ] || [ $$err -ne 0 ]; then exit 1; fi
324
325
# ---- RBS extractor golden tests ----
326
RBS_TEST_SRCS := $(sort $(wildcard test/rbs/*.rbs))
327
328
ifeq ($(wildcard $(RBS_INC)/rbs/parser.h),)
329
rbs-test:
330
	@echo "rbs-test: skipped (vendor/rbs not fetched; run 'make deps')"
331
regen-rbs-expected:
332
	@echo "regen-rbs-expected: skipped (vendor/rbs not fetched; run 'make deps')"
333
else
334
rbs-test: $(RBS_EXTRACT_BIN)
335
	@fail=0; n=0; \
336
	for f in $(RBS_TEST_SRCS); do \
337
	  n=$$((n+1)); \
338
	  exp="$${f%.rbs}.seed.expected"; \
339
	  if [ ! -f "$$exp" ]; then echo "rbs-test: MISSING golden $$exp"; fail=1; continue; fi; \
340
	  d=$$($(RBS_EXTRACT_BIN) "$$f" 2>/dev/null | diff -u "$$exp" - 2>&1); \
341
	  if [ -z "$$d" ]; then \
342
	    if [ -t 1 ]; then printf .; fi; \
343
	  else \
344
	    echo; echo "rbs-test FAIL: $$f"; echo "$$d"; fail=1; \
345
	  fi; \
346
	done; \
347
	if [ -t 1 ]; then printf '\n'; fi; \
348
	if [ $$fail -ne 0 ]; then echo "RBS extractor tests: FAIL"; exit 1; fi; \
349
	echo "RBS extractor tests: $$n pass"
350
351
regen-rbs-expected: $(RBS_EXTRACT_BIN)
352
	@for f in $(RBS_TEST_SRCS); do \
353
	  $(RBS_EXTRACT_BIN) "$$f" > "$${f%.rbs}.seed.expected"; \
354
	  echo "regen: $${f%.rbs}.seed.expected"; \
355
	done
356
endif
357
358
# End-to-end --rbs seeding check (#1417). The extractor emits a module-nested
359
# class under its qualified name (`Outer_Box`), but the compiler's class table
360
# stores the leaf name (`Box`) + enclosing_class. seed_class_index must match
361
# the two so the seed reaches the class. The fixture's `@label` is declared
362
# `String?` but only ever assigned nil, so inference alone leaves it poly --
363
# only an applied seed pins it to a `const char *` field. The extractor must sit
364
# beside $(SPINEL) (main.c looks for it as a sibling), so copy it there first.
365
ifeq ($(wildcard $(RBS_INC)/rbs/parser.h),)
366
rbs-seed-test:
367
	@echo "rbs-seed-test: skipped (vendor/rbs not fetched; run 'make deps')"
368
else
369
rbs-seed-test: $(SPINEL) $(RBS_EXTRACT_BIN) $(SP_RT_LIB)
370
	@cp -f $(RBS_EXTRACT_BIN) $(dir $(SPINEL))spinel_rbs_extract
371
	@tmp=$$(mktemp -d /tmp/spinel-rbsseed.XXXXXX); ok=1; \
372
	$(SPINEL) test/rbs-seed/nested_ivar.rb --rbs test/rbs-seed/sig \
373
	  -c --no-line-map -o "$$tmp/out.c" 2>/dev/null; \
374
	grep -Eq 'const char[[:space:]]+\*[[:space:]]*iv_label' "$$tmp/out.c" || { echo "rbs-seed-test: FAIL (#1417: module-nested-class seed not applied)"; ok=0; }; \
375
	if grep -Eq 'sp_RbVal[[:space:]]+iv_label' "$$tmp/out.c"; then echo "rbs-seed-test: FAIL (#1417: ivar stayed poly)"; ok=0; fi; \
376
	$(CC) -fsyntax-only -Ilib "$$tmp/out.c" 2>/dev/null || { echo "rbs-seed-test: FAIL (nested_ivar C invalid)"; ok=0; }; \
377
	$(SPINEL) test/rbs-seed/boundary.rb --rbs test/rbs-seed/sig \
378
	  -c --no-line-map -o "$$tmp/b.c" 2>/dev/null; \
379
	if $(CC) -O0 -Ilib "$$tmp/b.c" $(SP_RT_LIB) -lm -o "$$tmp/b" 2>"$$tmp/b.err"; then \
380
	  "$$tmp/b" > "$$tmp/b.out" 2>/dev/null; \
381
	  cmp -s "$$tmp/b.out" test/rbs-seed/boundary.expected || { echo "rbs-seed-test: FAIL (#1417 boundary output mismatch)"; diff -u test/rbs-seed/boundary.expected "$$tmp/b.out" || true; ok=0; }; \
382
	else echo "rbs-seed-test: FAIL (#1417 boundary coercion C did not compile)"; ok=0; fi; \
383
	$(SPINEL) test/rbs-seed/void_block_tail.rb --rbs test/rbs-seed/sig \
384
	  -c --no-line-map -o "$$tmp/v.c" 2>/dev/null; \
385
	if $(CC) -O0 -Ilib "$$tmp/v.c" $(SP_RT_LIB) -lm -o "$$tmp/v" 2>"$$tmp/v.err"; then \
386
	  "$$tmp/v" > "$$tmp/v.out" 2>/dev/null; \
387
	  cmp -s "$$tmp/v.out" test/rbs-seed/void_block_tail.expected || { echo "rbs-seed-test: FAIL (void block tail output mismatch)"; diff -u test/rbs-seed/void_block_tail.expected "$$tmp/v.out" || true; ok=0; }; \
388
	else echo "rbs-seed-test: FAIL (void-returning call as proc tail: C did not compile)"; ok=0; fi; \
389
	rm -rf "$$tmp"; \
390
	if [ $$ok -eq 1 ]; then echo "rbs-seed-test: pass"; else exit 1; fi
391
endif
392
393
# Analyze-fail fixtures test the legacy analyzer's rejection diagnostics;
394
# the recipe lives in legacy/Makefile. Thin delegator for `make gate`.
395
analyze-fail-test:
396
	+@$(MAKE) -C legacy --no-print-directory analyze-fail-test
397
398
# The .ok target is the test's stamp. Order-only $(SPINEL) keeps a
399
# compiler relink from invalidating every test.
400
build/test-results/%.ok: test/%.rb $(SP_RT_LIB) | $(SPINEL)
401
	@mkdir -p build/test-results
402
	@tmpdir=$$(mktemp -d /tmp/spinel-test.XXXXXX); \
403
	ast=$$tmpdir/test.ast; \
404
	ir=$$tmpdir/test.ir; \
405
	cfile=$$tmpdir/test.c; \
406
	bin=$$tmpdir/test_bin; \
407
	exp=$$tmpdir/expected; \
408
	act=$$tmpdir/actual; \
409
	args=""; \
410
	if [ -f "$<.args" ]; then args=$$(cat "$<.args"); fi; \
411
	rm -f "[email protected]"; \
412
	$(SPINEL) "$<" $(SP_OV_FLAG) -c --no-line-map -o "$$cfile" 2>/dev/null && \
413
	$(CC) $(CFLAGS) $(SP_OV_DEFINE) -Werror $(TEST_WARN_SUPPRESS) $(SEC_FLAGS) -Ilib "$$cfile" $(SP_RT_LIB) $(LDFLAGS) -lm $(GC_FLAGS) -o "$$bin" 2>/dev/null; \
414
	if [ $$? -eq 0 ]; then \
415
	  if [ -f "$<.expected" ]; then \
416
	    LC_ALL=C sed 's/\r$$//' "$<.expected" >"$$exp.n"; \
417
	  else \
418
	    $(TIMEOUT10) $(REF_RUBY) "$<" $$args >"$$exp" 2>/dev/null; \
419
	    ruby_rc=$$?; \
420
	    if [ $$ruby_rc -ne 0 ] && [ "$(REF_RUBY)" != "ruby" ]; then \
421
	      $(TIMEOUT10) ruby "$<" $$args >"$$exp" 2>/dev/null; \
422
	    fi; \
423
	    LC_ALL=C sed 's/\r$$//' "$$exp" >"$$exp.n"; \
424
	  fi; \
425
	  $(TIMEOUT10) "$$bin" $$args >"$$act" 2>/dev/null; \
426
	  LC_ALL=C sed 's/\r$$//' "$$act" >"$$act.n"; \
427
	  if cmp -s "$$exp.n" "$$act.n"; then \
428
	    echo PASS > "$@"; \
429
	    if [ -t 1 ]; then printf .; fi; \
430
	  else \
431
	    echo FAIL > "$@"; \
432
	    diff -u "$$exp.n" "$$act.n" > "[email protected]" 2>&1 || true; \
433
	    if [ -t 1 ]; then printf F; fi; \
434
	  fi; \
435
	else \
436
	  echo ERR > "$@"; \
437
	  if [ -t 1 ]; then printf E; fi; \
438
	fi; \
439
	rm -rf "$$tmpdir"
440
441
clean-test-results:
442
	@rm -rf build/test-results build/analyze-fail-results
443
444
# ---- Expected-output regeneration ----
445
# Capture each test's reference Ruby output into test/<name>.rb.expected;
446
# once committed the test target uses it directly and skips per-test ruby.
447
EXPECTED_FILES := $(patsubst test/%.rb,test/%.rb.expected,$(TESTS))
448
449
regen-expected: $(EXPECTED_FILES)
450
451
test/%.rb.expected: test/%.rb
452
	@args=""; \
453
	if [ -f "$<.args" ]; then args=$$(cat "$<.args"); fi; \
454
	$(TIMEOUT10) $(REF_RUBY) $< $$args >[email protected] 2>/dev/null; \
455
	rc=$$?; \
456
	if [ $$rc -ne 0 ] && [ "$(REF_RUBY)" != "ruby" ]; then \
457
	  $(TIMEOUT10) ruby $< $$args >[email protected] 2>/dev/null; \
458
	  rc=$$?; \
459
	fi; \
460
	if [ $$rc -ne 0 ]; then \
461
	  echo "regen-expected: $< failed (rc=$$rc); skipping" >&2; \
462
	  rm -f [email protected]; \
463
	else \
464
	  LC_ALL=C sed 's/\r$$//' [email protected] > $@; \
465
	  rm -f [email protected]; \
466
	fi
467
468
bench: $(SPINEL) $(SP_RT_LIB)
469
	@if [ -z "$(TIMEOUT_BIN)" ]; then echo "Note: no 'timeout' command found; running without time limits."; fi
470
	@total=$$(ls benchmark/*.rb | wc -l); \
471
	if [ -t 1 ]; then tty=1; else tty=0; fi; \
472
	pass=0; fail=0; err=0; skip=0; i=0; \
473
	for f in benchmark/*.rb; do \
474
	  i=$$((i+1)); \
475
	  bn=$$(basename "$$f" .rb); \
476
	  if [ "$$tty" = 1 ]; then printf '\r\033[K  [%d/%d] %s' "$$i" "$$total" "$$bn"; fi; \
477
	  $(TIMEOUT10) $(SPINEL) "$$f" -c --no-line-map -o /tmp/_sp_b.c 2>/dev/null && \
478
	  $(CC) $(CFLAGS) -Werror $(TEST_WARN_SUPPRESS) $(SEC_FLAGS) -Ilib -c /tmp/_sp_b.c -o /tmp/_sp_b.c.o 2>/dev/null && \
479
	  $(CC) $(CFLAGS) /tmp/_sp_b.c.o $(SP_RT_LIB) $(LDFLAGS) -lm $(GC_FLAGS) -o /tmp/_sp_b_bin 2>/dev/null; \
480
	  if [ $$? -eq 0 ]; then \
481
	    $(TIMEOUT60) $(REF_RUBY) "$$f" >/tmp/_sp_b_exp 2>/dev/null; \
482
	    ruby_rc=$$?; \
483
	    if [ $$ruby_rc -ne 0 ] && [ "$(REF_RUBY)" != "ruby" ]; then \
484
	      $(TIMEOUT60) ruby "$$f" >/tmp/_sp_b_exp 2>/dev/null; \
485
	      ruby_rc=$$?; \
486
	    fi; \
487
	    if [ $$ruby_rc -eq 124 ]; then \
488
	      if [ "$$tty" = 1 ]; then printf '\r\033[K'; fi; \
489
	      echo "SKIP: $$bn (ruby timeout)"; skip=$$((skip+1)); \
490
	    else \
491
	      $(TIMEOUT60) /tmp/_sp_b_bin >/tmp/_sp_b_act 2>/dev/null; \
492
	      LC_ALL=C sed 's/\r$$//' /tmp/_sp_b_exp >/tmp/_sp_b_exp.n; \
493
	      LC_ALL=C sed 's/\r$$//' /tmp/_sp_b_act >/tmp/_sp_b_act.n; \
494
	      if cmp -s /tmp/_sp_b_exp.n /tmp/_sp_b_act.n; then \
495
	        pass=$$((pass+1)); \
496
	      else \
497
	        if [ "$$tty" = 1 ]; then printf '\r\033[K'; fi; \
498
	        echo "FAIL: $$bn"; \
499
	        diff -u /tmp/_sp_b_exp.n /tmp/_sp_b_act.n 2>&1 | head -40; \
500
	        fail=$$((fail+1)); \
501
	      fi; \
502
	    fi; \
503
	  else \
504
	    if [ "$$tty" = 1 ]; then printf '\r\033[K'; fi; \
505
	    echo "ERR:  $$bn"; err=$$((err+1)); \
506
	  fi; \
507
	done; \
508
	if [ "$$tty" = 1 ]; then printf '\r\033[K'; fi; \
509
	rm -f /tmp/_sp_b.ast /tmp/_sp_b.ir /tmp/_sp_b.c /tmp/_sp_b.c.o /tmp/_sp_b_bin /tmp/_sp_b_exp /tmp/_sp_b_act /tmp/_sp_b_exp.n /tmp/_sp_b_act.n; \
510
	echo "Benchmarks: $$pass pass, $$fail fail, $$err error, $$skip skip"; \
511
	if [ $$fail -ne 0 ] || [ $$err -ne 0 ]; then exit 1; fi
512
513
# ---- Optcarrot integration test ----
514
OPTCARROT_DIR  := build/optcarrot
515
OPTCARROT_REPO := https://github.com/mame/optcarrot.git
516
OPTCARROT_BRANCH := experiment/spinel
517
518
optcarrot: $(SPINEL) $(SP_RT_LIB)
519
	@if [ ! -d $(OPTCARROT_DIR) ]; then \
520
	  git clone --depth=1 --branch=$(OPTCARROT_BRANCH) $(OPTCARROT_REPO) $(OPTCARROT_DIR); \
521
	fi
522
	@ruby $(OPTCARROT_DIR)/tools/pack-for-spinel.rb > build/optcarrot-single.rb
523
	@$(SPINEL) build/optcarrot-single.rb -c --no-line-map -o build/optcarrot-single.c
524
	@$(CC) $(CFLAGS) -DSP_INT_OVERFLOW_MODE_WRAP -Ilib build/optcarrot-single.c $(SP_RT_LIB) $(LDFLAGS) -lm $(GC_FLAGS) -o build/optcarrot-single
525
	@out=$$($(TIMEOUT60) ./build/optcarrot-single 2>&1); \
526
	echo "$$out"; \
527
	if echo "$$out" | grep -qE "^fps: [0-9.]+$$" && echo "$$out" | grep -q "^checksum: 59662$$"; then \
528
	  echo "Optcarrot: OK"; \
529
	else \
530
	  echo "Optcarrot: FAIL β€” expected 'fps: <num>' and 'checksum: 59662'"; \
531
	  exit 1; \
532
	fi
533
534
# ---- Developer gates ----
535
#
536
# `test`, `bench` and `optcarrot` only READ the compiler binaries and
537
# write to disjoint build/ dirs, so they run concurrently as parallel
538
# prerequisites. Every recursive $(MAKE) is `+`-prefixed so the jobserver
539
# fd is inherited; none pass an explicit -j (which would force a sub-make
540
# to spawn its own pool β†’ oversubscription).
541
542
# Fast pre-commit: rebuild the compiler and run the suite. OPT=-O1 compiles
543
# the sp_runtime.h-heavy per-test C ~3x faster than -O0 (the optimizer prunes
544
# the 800+ unreferenced static fns before codegen). Skips bench/optcarrot β€”
545
# run `make gate` before pushing for those.
546
check:
547
	+@$(MAKE) --no-print-directory LTO=0 all
548
	+@$(MAKE) --no-print-directory test OPT=-O1
549
550
# Full pre-push gate: test || bench || optcarrot in parallel. The C compiler is
551
# master; the legacy Ruby self-host bootstrap and its analyze-fail diagnostics
552
# are NOT part of the gate (run `make bootstrap` / `cd legacy && make` explicitly
553
# for those).
554
gate:
555
	+@$(MAKE) --no-print-directory LTO=0 all
556
	+@$(MAKE) --no-print-directory gate-legs
557
	@echo "gate: ALL GREEN"
558
559
gate-legs: gate-test gate-bench gate-optcarrot
560
gate-test:
561
	+@$(MAKE) --no-print-directory test OPT=-O1
562
gate-bench:
563
	+@$(MAKE) --no-print-directory bench
564
gate-optcarrot:
565
	+@$(MAKE) --no-print-directory optcarrot
566
567
# ---- Install ----
568
569
PREFIX   ?= /usr/local
570
SPNLDIR   = $(PREFIX)/lib/spinel
571
572
# Install the single `spinel` binary only. The legacy Ruby compiler is a
573
# headless regression oracle (legacy/, `make legacy`/`bootstrap`/
574
# `analyze-fail-test`) and is not shipped.
575
install: all
576
	install -d $(SPNLDIR)/lib
577
	install -m 755 $(SPINEL)            $(SPNLDIR)/spinel
578
	install -m 644 lib/libspinel_rt.a    $(SPNLDIR)/lib/
579
	install -m 644 lib/sp_runtime.h      $(SPNLDIR)/lib/
580
	install -m 644 lib/sp_types.h        $(SPNLDIR)/lib/
581
	install -m 644 lib/sp_core.h         $(SPNLDIR)/lib/
582
	install -m 644 lib/sp_system.h       $(SPNLDIR)/lib/
583
	install -m 644 lib/sp_gc.h           $(SPNLDIR)/lib/
584
	install -m 644 lib/sp_fiber.h         $(SPNLDIR)/lib/
585
	install -m 644 lib/sp_io.h           $(SPNLDIR)/lib/
586
	install -m 644 lib/sp_time.h         $(SPNLDIR)/lib/
587
	install -m 644 lib/sp_net.h          $(SPNLDIR)/lib/
588
	install -m 644 lib/*.rb              $(SPNLDIR)/lib/
589
	install -d $(PREFIX)/bin
590
	ln -sf $(SPNLDIR)/spinel $(PREFIX)/bin/spinel
591
	for t in $(TOOL_NAMES); do \
592
	  install -m 755 bin/spinel-$$t $(PREFIX)/bin/spinel-$$t; \
593
	done
594
595
uninstall:
596
	rm -f $(PREFIX)/bin/spinel
597
	for t in $(TOOL_NAMES); do rm -f $(PREFIX)/bin/spinel-$$t; done
598
	rm -rf $(SPNLDIR)
599
600
# ---- Clean ----
601
602
# Wipe the root build tree plus the legacy subtree's own build dir
603
# (legacy/build holds the legacy compiler's binaries + bootstrap
604
# intermediates). Only the root-level C artifacts need an explicit rm.
605
clean:
606
	rm -rf build/ bin/ legacy/build/
607
	rm -f legacy/spinel_parse spinel