aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-17 09:23:04 +0000
committer_ov4 <ov4@ov4.org>2026-10-17 09:23:04 +0000
commit50186a23cd62fc6ce120f6114b428dc5c26b5a56 (patch)
treefb36447a7f8c932fcf2cd745901b538912eacfae
parent9467ae311ddc0ce8b4c329f3dbcec88fb279719d (diff)
test(multifile): cover imports, shared globals and exception propagation
Allow fixtures to pass ordered source lists and require file-qualified negative diagnostics. Add native and runner cases for cross-unit calls, global mutation and shadowing, transitive imports, types, MAIN status, exception catches, runtime failures and destructor unwinding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS/TESTING.md8
-rw-r--r--tests/README.md8
-rw-r--r--tests/i/multifile-functions-globals/case.json6
-rw-r--r--tests/i/multifile-functions-globals/expected.txt6
-rw-r--r--tests/i/multifile-functions-globals/lib.cps27
-rw-r--r--tests/i/multifile-functions-globals/notes.md5
-rw-r--r--tests/i/multifile-functions-globals/program.cps11
-rw-r--r--tests/i/multifile-main-exit/case.json7
-rw-r--r--tests/i/multifile-main-exit/expected.txt1
-rw-r--r--tests/i/multifile-main-exit/lib.cps9
-rw-r--r--tests/i/multifile-main-exit/notes.md5
-rw-r--r--tests/i/multifile-main-exit/program.cps6
-rw-r--r--tests/i/multifile-runtime-catch/case.json6
-rw-r--r--tests/i/multifile-runtime-catch/expected.txt3
-rw-r--r--tests/i/multifile-runtime-catch/lib.cps3
-rw-r--r--tests/i/multifile-runtime-catch/notes.md5
-rw-r--r--tests/i/multifile-runtime-catch/program.cps7
-rw-r--r--tests/i/multifile-transitive-types/bridge.cps10
-rw-r--r--tests/i/multifile-transitive-types/case.json7
-rw-r--r--tests/i/multifile-transitive-types/expected.txt2
-rw-r--r--tests/i/multifile-transitive-types/nested/values.cps6
-rw-r--r--tests/i/multifile-transitive-types/notes.md5
-rw-r--r--tests/i/multifile-transitive-types/program.cps8
-rw-r--r--tests/i/multifile-try-catch/case.json7
-rw-r--r--tests/i/multifile-try-catch/expected.txt7
-rw-r--r--tests/i/multifile-try-catch/leaf.cps15
-rw-r--r--tests/i/multifile-try-catch/middle.cps13
-rw-r--r--tests/i/multifile-try-catch/notes.md5
-rw-r--r--tests/i/multifile-try-catch/program.cps13
-rw-r--r--tests/i/multifile-uncaught/case.json7
-rw-r--r--tests/i/multifile-uncaught/expected.txt1
-rw-r--r--tests/i/multifile-uncaught/lib.cps3
-rw-r--r--tests/i/multifile-uncaught/notes.md5
-rw-r--r--tests/i/multifile-uncaught/program.cps5
-rw-r--r--tests/iii/multifile-body-location/case.json13
-rw-r--r--tests/iii/multifile-body-location/lib.cps3
-rw-r--r--tests/iii/multifile-body-location/program.cps2
-rw-r--r--tests/iii/multifile-duplicate-constant/case.json13
-rw-r--r--tests/iii/multifile-duplicate-constant/lib.cps1
-rw-r--r--tests/iii/multifile-duplicate-constant/program.cps2
-rw-r--r--tests/iii/multifile-duplicate-function/case.json13
-rw-r--r--tests/iii/multifile-duplicate-function/lib.cps3
-rw-r--r--tests/iii/multifile-duplicate-function/program.cps4
-rw-r--r--tests/iii/multifile-global-array/case.json13
-rw-r--r--tests/iii/multifile-global-array/lib.cps3
-rw-r--r--tests/iii/multifile-global-array/program.cps1
-rw-r--r--tests/iii/multifile-global-call/case.json13
-rw-r--r--tests/iii/multifile-global-call/lib.cps7
-rw-r--r--tests/iii/multifile-global-call/program.cps1
-rw-r--r--tests/iii/multifile-global-expression/case.json13
-rw-r--r--tests/iii/multifile-global-expression/lib.cps4
-rw-r--r--tests/iii/multifile-global-expression/program.cps1
-rw-r--r--tests/iii/multifile-hidden-symbols/case.json28
-rw-r--r--tests/iii/multifile-hidden-symbols/lib.cps8
-rw-r--r--tests/iii/multifile-hidden-symbols/program.cps4
-rw-r--r--tests/iii/multifile-invalid-main/case.json13
-rw-r--r--tests/iii/multifile-invalid-main/lib.cps1
-rw-r--r--tests/iii/multifile-invalid-main/program.cps3
-rw-r--r--tests/iii/multifile-lexer-location/case.json13
-rw-r--r--tests/iii/multifile-lexer-location/lib.cps1
-rw-r--r--tests/iii/multifile-lexer-location/program.cps1
-rw-r--r--tests/iii/multifile-main-and-code/case.json13
-rw-r--r--tests/iii/multifile-main-and-code/lib.cps1
-rw-r--r--tests/iii/multifile-main-and-code/program.cps3
-rw-r--r--tests/iii/multifile-missing-include/case.json13
-rw-r--r--tests/iii/multifile-missing-include/lib.cps1
-rw-r--r--tests/iii/multifile-missing-include/program.cps2
-rw-r--r--tests/iii/multifile-no-entry/case.json13
-rw-r--r--tests/iii/multifile-no-entry/lib.cps2
-rw-r--r--tests/iii/multifile-no-entry/program.cps1
-rw-r--r--tests/iii/multifile-parser-location/case.json13
-rw-r--r--tests/iii/multifile-parser-location/lib.cps3
-rw-r--r--tests/iii/multifile-parser-location/program.cps1
-rw-r--r--tests/iii/multifile-two-entries/case.json13
-rw-r--r--tests/iii/multifile-two-entries/lib.cps1
-rw-r--r--tests/iii/multifile-two-entries/program.cps1
-rw-r--r--tests/run.pl23
-rw-r--r--tests/test_runner.pl17
78 files changed, 539 insertions, 6 deletions
diff --git a/AGENTS/TESTING.md b/AGENTS/TESTING.md
index 1234408..0be845c 100644
--- a/AGENTS/TESTING.md
+++ b/AGENTS/TESTING.md
@@ -4,6 +4,14 @@ Compiler tests live in `tests/`; run instructions are in its README. `BUILD_TEST
Each case directory has `program.cps` and `case.json`. Suite i adds `expected.txt`, optional `input.txt`, input files under `files/`, expected text-file contents under `expected-files/`, and independently written trace notes with human-review status. Suite ii adds `reference.cc` and `generate.pl`; the generator accepts seed and random count, emits the total record count followed by one ASCII record per line, and each program emits one result line per record. Suite iii stores an `errors` list of `{reason: regex, line: integer}` in its manifest. A cause and its line must appear on the same error diagnostic. EOF after a final newline is the following source line.
+Multi-file cases specify `sources` as an ordered array of paths relative to the
+case directory, defaulting to `["program.cps"]`. Library sources can be in nested
+subdirectories. Both compilation and runner commands receive the whole list.
+Suite iii error expectations may also specify `file` relative to the case directory;
+the reason, line and exact absolute input path must match the same diagnostic.
+The harness contract tests reject wrong filenames, filename prefixes and split
+reason/location messages.
+
Optional manifest fields are `exit_code` for expected program status (default 0) and `timeout` for execution/reference seconds (default 30; file-intensive cases use 180). Compilation has a 120-second timeout. Do not make a known compiler defect pass by changing the expectation, adding an exemption or comparing only a permissive substring of program output.
The Perl runner (`tests/run.pl`) accepts `--case`, `--cpsc`, `--run-mode`, `--perl`, `--reference`, `--seed`, `--count` and `--results`. CMake provides paths, including executable suffixes, and caches `CPSC_TEST_SEED` and `CPSC_TEST_COUNT`. The default seed is HEAD read at execution, not configure time. CI overrides it with the checked-out push SHA. Both native and enabled runner paths are tested independently; NONE skips only runner execution. Windows CI must configure a native `PERL_EXECUTABLE` (Strawberry Perl) because MSYS perl cannot read the native `D:/` drive paths CMake passes.
diff --git a/tests/README.md b/tests/README.md
index b617161..b186aec 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -29,3 +29,11 @@ Use the actual SHA from a failed report in place of `FULL_COMMIT_SHA`. Clear `CP
In Actions, each compiler platform runs all three suites and summarizes results before its existing package smoke checks and upload. Failures retain inputs and logs as an Actions artifact for 14 days. Each platform publishes only after its own tests pass. The initial Linux failures have been fixed; any failing compiler test still blocks publication for that platform. The independent plugin job can publish separately.
For adding cases and runner interfaces, see [AGENTS/TESTING.md](../AGENTS/TESTING.md).
+
+Multi-file fixtures use a `sources` array in `case.json`, listing all compiler inputs relative to the case directory in command-line order. The default remains `["program.cps"]`. Included libraries live beside the fixture source or in subdirectories; INCLUDE paths stay relative to their including source. Sources are passed separately to both native compilation and the configured runner. Negative cases may add `file` to an error expectation; the reason, line and exact resolved source path must occur on the same diagnostic.
+
+Run the cross-file cases (functions, shared globals, imported types, include cycles, MAIN, exception unwinding and rejection diagnostics) with:
+
+```sh
+ctest --test-dir build -R multifile --output-on-failure
+```
diff --git a/tests/i/multifile-functions-globals/case.json b/tests/i/multifile-functions-globals/case.json
new file mode 100644
index 0000000..032ec6a
--- /dev/null
+++ b/tests/i/multifile-functions-globals/case.json
@@ -0,0 +1,6 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ]
+}
diff --git a/tests/i/multifile-functions-globals/expected.txt b/tests/i/multifile-functions-globals/expected.txt
new file mode 100644
index 0000000..86906d2
--- /dev/null
+++ b/tests/i/multifile-functions-globals/expected.txt
@@ -0,0 +1,6 @@
+3
+3:shared:TRUE:Q:TRUE
+5
+40
+5
+99:7
diff --git a/tests/i/multifile-functions-globals/lib.cps b/tests/i/multifile-functions-globals/lib.cps
new file mode 100644
index 0000000..0d09ad5
--- /dev/null
+++ b/tests/i/multifile-functions-globals/lib.cps
@@ -0,0 +1,27 @@
+CONSTANT Offset = 2
+FUNCTION Add(a : INTEGER, b : INTEGER) RETURNS INTEGER
+ RETURN a + b
+ENDFUNCTION
+PROCEDURE Increment()
+ Counter <- Counter + Offset
+ENDPROCEDURE
+PROCEDURE Shadow()
+ DECLARE Counter : INTEGER
+ Counter <- 40
+ OUTPUT Counter
+ENDPROCEDURE
+FUNCTION ReadCounter RETURNS INTEGER
+ RETURN Counter
+ENDFUNCTION
+GLOBAL
+ DECLARE Counter : INTEGER
+ Counter <- 3
+ DECLARE Label : STRING
+ Label <- "shared"
+ DECLARE Enabled : BOOLEAN
+ Enabled <- TRUE
+ DECLARE Initial : CHAR
+ Initial <- 'Q'
+ DECLARE Ratio : REAL
+ Ratio <- -1.5
+ENDGLOBAL
diff --git a/tests/i/multifile-functions-globals/notes.md b/tests/i/multifile-functions-globals/notes.md
new file mode 100644
index 0000000..632c848
--- /dev/null
+++ b/tests/i/multifile-functions-globals/notes.md
@@ -0,0 +1,5 @@
+# Multi-file regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+These are compiler extensions, not Cambridge guide requirements. Add returns 3. Globals start at 3/shared/TRUE/Q/-1.5. Increment adds the library constant 2. Shadow prints its local 40 without changing the global 5. The entry local 99 survives another library increment; ReadCounter returns the shared 7. Functions precede GLOBAL declarations to check pre-pass visibility.
diff --git a/tests/i/multifile-functions-globals/program.cps b/tests/i/multifile-functions-globals/program.cps
new file mode 100644
index 0000000..e5e6a6c
--- /dev/null
+++ b/tests/i/multifile-functions-globals/program.cps
@@ -0,0 +1,11 @@
+INCLUDE "lib.cps"
+OUTPUT Add(1, 2)
+OUTPUT Counter, ":", Label, ":", Enabled, ":", Initial, ":", Ratio = -1.5
+CALL Increment()
+OUTPUT Counter
+CALL Shadow()
+OUTPUT Counter
+DECLARE Counter : INTEGER
+Counter <- 99
+CALL Increment()
+OUTPUT Counter, ":", ReadCounter()
diff --git a/tests/i/multifile-main-exit/case.json b/tests/i/multifile-main-exit/case.json
new file mode 100644
index 0000000..3cfd155
--- /dev/null
+++ b/tests/i/multifile-main-exit/case.json
@@ -0,0 +1,7 @@
+{
+ "sources": [
+ "lib.cps",
+ "program.cps"
+ ],
+ "exit_code": 3
+}
diff --git a/tests/i/multifile-main-exit/expected.txt b/tests/i/multifile-main-exit/expected.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/tests/i/multifile-main-exit/expected.txt
@@ -0,0 +1 @@
+1
diff --git a/tests/i/multifile-main-exit/lib.cps b/tests/i/multifile-main-exit/lib.cps
new file mode 100644
index 0000000..2bb4941
--- /dev/null
+++ b/tests/i/multifile-main-exit/lib.cps
@@ -0,0 +1,9 @@
+GLOBAL
+ DECLARE Counter : INTEGER
+ENDGLOBAL
+PROCEDURE Increment()
+ Counter <- Counter + 1
+ENDPROCEDURE
+FUNCTION Status RETURNS INTEGER
+ RETURN 4294967298 + Counter
+ENDFUNCTION
diff --git a/tests/i/multifile-main-exit/notes.md b/tests/i/multifile-main-exit/notes.md
new file mode 100644
index 0000000..486abab
--- /dev/null
+++ b/tests/i/multifile-main-exit/notes.md
@@ -0,0 +1,5 @@
+# Multi-file regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+These are compiler extensions, not Cambridge guide requirements. The entry is the second input and its name has mixed case. Increment changes the shared counter from 0 to 1. Status returns 4294967299, whose low 32 bits are 3; both native execution and the configured runner must return status 3.
diff --git a/tests/i/multifile-main-exit/program.cps b/tests/i/multifile-main-exit/program.cps
new file mode 100644
index 0000000..928d4df
--- /dev/null
+++ b/tests/i/multifile-main-exit/program.cps
@@ -0,0 +1,6 @@
+INCLUDE "lib.cps"
+FUNCTION mAiN RETURNS INTEGER
+ CALL Increment()
+ OUTPUT Counter
+ RETURN Status()
+ENDFUNCTION
diff --git a/tests/i/multifile-runtime-catch/case.json b/tests/i/multifile-runtime-catch/case.json
new file mode 100644
index 0000000..032ec6a
--- /dev/null
+++ b/tests/i/multifile-runtime-catch/case.json
@@ -0,0 +1,6 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ]
+}
diff --git a/tests/i/multifile-runtime-catch/expected.txt b/tests/i/multifile-runtime-catch/expected.txt
new file mode 100644
index 0000000..731df7f
--- /dev/null
+++ b/tests/i/multifile-runtime-catch/expected.txt
@@ -0,0 +1,3 @@
+[Fatal] line 2: Division by zero
+runtime caught
+5
diff --git a/tests/i/multifile-runtime-catch/lib.cps b/tests/i/multifile-runtime-catch/lib.cps
new file mode 100644
index 0000000..694eaf6
--- /dev/null
+++ b/tests/i/multifile-runtime-catch/lib.cps
@@ -0,0 +1,3 @@
+FUNCTION Quotient(Divisor : INTEGER) RETURNS INTEGER
+ RETURN 10 DIV Divisor
+ENDFUNCTION
diff --git a/tests/i/multifile-runtime-catch/notes.md b/tests/i/multifile-runtime-catch/notes.md
new file mode 100644
index 0000000..d3a118f
--- /dev/null
+++ b/tests/i/multifile-runtime-catch/notes.md
@@ -0,0 +1,5 @@
+# Cross-file runtime-check regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+TRY/CATCH and multi-file builds are compiler extensions. Quotient(0) fails at the division on library line 2, printing the exact runtime diagnostic before throwing. The entry catches it and continues. Quotient(2) then returns 5, proving the handler leaves execution usable.
diff --git a/tests/i/multifile-runtime-catch/program.cps b/tests/i/multifile-runtime-catch/program.cps
new file mode 100644
index 0000000..3d11099
--- /dev/null
+++ b/tests/i/multifile-runtime-catch/program.cps
@@ -0,0 +1,7 @@
+INCLUDE "lib.cps"
+TRY
+ OUTPUT Quotient(0)
+CATCH
+ OUTPUT "runtime caught"
+ENDTRY
+OUTPUT Quotient(2)
diff --git a/tests/i/multifile-transitive-types/bridge.cps b/tests/i/multifile-transitive-types/bridge.cps
new file mode 100644
index 0000000..cbdcae8
--- /dev/null
+++ b/tests/i/multifile-transitive-types/bridge.cps
@@ -0,0 +1,10 @@
+INCLUDE "nested/values.cps"
+CLASS Container
+ Value : INTEGER
+ FUNCTION Read RETURNS INTEGER
+ RETURN Value + Base
+ ENDFUNCTION
+ENDCLASS
+TYPE Pair
+ DECLARE Value : INTEGER
+ENDTYPE
diff --git a/tests/i/multifile-transitive-types/case.json b/tests/i/multifile-transitive-types/case.json
new file mode 100644
index 0000000..445f846
--- /dev/null
+++ b/tests/i/multifile-transitive-types/case.json
@@ -0,0 +1,7 @@
+{
+ "sources": [
+ "program.cps",
+ "bridge.cps",
+ "nested/values.cps"
+ ]
+}
diff --git a/tests/i/multifile-transitive-types/expected.txt b/tests/i/multifile-transitive-types/expected.txt
new file mode 100644
index 0000000..a14bbdb
--- /dev/null
+++ b/tests/i/multifile-transitive-types/expected.txt
@@ -0,0 +1,2 @@
+11:6:0
+11
diff --git a/tests/i/multifile-transitive-types/nested/values.cps b/tests/i/multifile-transitive-types/nested/values.cps
new file mode 100644
index 0000000..713ffce
--- /dev/null
+++ b/tests/i/multifile-transitive-types/nested/values.cps
@@ -0,0 +1,6 @@
+INCLUDE "../bridge.cps"
+CONSTANT Base = 3
+TYPE Choice = (First, Second)
+FUNCTION Twice(n : INTEGER) RETURNS INTEGER
+ RETURN n * 2
+ENDFUNCTION
diff --git a/tests/i/multifile-transitive-types/notes.md b/tests/i/multifile-transitive-types/notes.md
new file mode 100644
index 0000000..160c801
--- /dev/null
+++ b/tests/i/multifile-transitive-types/notes.md
@@ -0,0 +1,5 @@
+# Multi-file regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+These are compiler extensions, not Cambridge guide requirements. The bridge imports nested values and the nested file imports the bridge, forming a cycle; repeated imports must not duplicate definitions. The entry sees Base=3, Twice and the enum through the bridge. Container.Read computes 8+3=11, Twice(3)=6, First has ordinal 0, and Pair stores 11. This covers imported methods, records, enums and constants, plus relative nested paths.
diff --git a/tests/i/multifile-transitive-types/program.cps b/tests/i/multifile-transitive-types/program.cps
new file mode 100644
index 0000000..9f4e6bd
--- /dev/null
+++ b/tests/i/multifile-transitive-types/program.cps
@@ -0,0 +1,8 @@
+INCLUDE "bridge.cps"
+INCLUDE "bridge.cps"
+DECLARE Item : Container
+Item.Value <- 8
+OUTPUT Item.Read(), ":", Twice(Base), ":", First
+DECLARE Node : Pair
+Node.Value <- Item.Read()
+OUTPUT Node.Value
diff --git a/tests/i/multifile-try-catch/case.json b/tests/i/multifile-try-catch/case.json
new file mode 100644
index 0000000..edcaa8c
--- /dev/null
+++ b/tests/i/multifile-try-catch/case.json
@@ -0,0 +1,7 @@
+{
+ "sources": [
+ "program.cps",
+ "middle.cps",
+ "leaf.cps"
+ ]
+}
diff --git a/tests/i/multifile-try-catch/expected.txt b/tests/i/multifile-try-catch/expected.txt
new file mode 100644
index 0000000..3560fb1
--- /dev/null
+++ b/tests/i/multifile-try-catch/expected.txt
@@ -0,0 +1,7 @@
+destroy:2
+destroy:1
+caught:2
+continued
+destroy:2
+library caught:3
+returned
diff --git a/tests/i/multifile-try-catch/leaf.cps b/tests/i/multifile-try-catch/leaf.cps
new file mode 100644
index 0000000..44b991a
--- /dev/null
+++ b/tests/i/multifile-try-catch/leaf.cps
@@ -0,0 +1,15 @@
+GLOBAL
+ DECLARE Destroyed : INTEGER
+ENDGLOBAL
+CLASS Guard
+ Id : INTEGER
+ PROCEDURE DELETE()
+ OUTPUT "destroy:", Id
+ Destroyed <- Destroyed + 1
+ ENDPROCEDURE
+ENDCLASS
+PROCEDURE Raise()
+ DECLARE GuardTwo : Guard
+ GuardTwo.Id <- 2
+ THROW
+ENDPROCEDURE
diff --git a/tests/i/multifile-try-catch/middle.cps b/tests/i/multifile-try-catch/middle.cps
new file mode 100644
index 0000000..11b8a85
--- /dev/null
+++ b/tests/i/multifile-try-catch/middle.cps
@@ -0,0 +1,13 @@
+INCLUDE "leaf.cps"
+PROCEDURE Outer()
+ DECLARE GuardOne : Guard
+ GuardOne.Id <- 1
+ CALL Raise()
+ENDPROCEDURE
+PROCEDURE CatchHere()
+ TRY
+ CALL Raise()
+ CATCH
+ OUTPUT "library caught:", Destroyed
+ ENDTRY
+ENDPROCEDURE
diff --git a/tests/i/multifile-try-catch/notes.md b/tests/i/multifile-try-catch/notes.md
new file mode 100644
index 0000000..cf7f961
--- /dev/null
+++ b/tests/i/multifile-try-catch/notes.md
@@ -0,0 +1,5 @@
+# Multi-file regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+These are compiler extensions, not Cambridge guide requirements. Raise destroys guard 2, then unwinding Outer destroys guard 1 before the entry catch reads Destroyed=2. Execution continues. CatchHere catches another Raise inside the middle file; only guard 2 is destroyed, yielding Destroyed=3, and the entry outer catch must not run. Every throw crosses a unit boundary and cleanup runs before catch output.
diff --git a/tests/i/multifile-try-catch/program.cps b/tests/i/multifile-try-catch/program.cps
new file mode 100644
index 0000000..ec3edca
--- /dev/null
+++ b/tests/i/multifile-try-catch/program.cps
@@ -0,0 +1,13 @@
+INCLUDE "middle.cps"
+TRY
+ CALL Outer()
+CATCH
+ OUTPUT "caught:", Destroyed
+ENDTRY
+OUTPUT "continued"
+TRY
+ CALL CatchHere()
+ OUTPUT "returned"
+CATCH
+ OUTPUT "wrong handler"
+ENDTRY
diff --git a/tests/i/multifile-uncaught/case.json b/tests/i/multifile-uncaught/case.json
new file mode 100644
index 0000000..bd1e132
--- /dev/null
+++ b/tests/i/multifile-uncaught/case.json
@@ -0,0 +1,7 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "exit_code": 1
+}
diff --git a/tests/i/multifile-uncaught/expected.txt b/tests/i/multifile-uncaught/expected.txt
new file mode 100644
index 0000000..e9431ec
--- /dev/null
+++ b/tests/i/multifile-uncaught/expected.txt
@@ -0,0 +1 @@
+Unhandled exception. Program terminated.
diff --git a/tests/i/multifile-uncaught/lib.cps b/tests/i/multifile-uncaught/lib.cps
new file mode 100644
index 0000000..c482a6d
--- /dev/null
+++ b/tests/i/multifile-uncaught/lib.cps
@@ -0,0 +1,3 @@
+PROCEDURE Fail()
+ THROW
+ENDPROCEDURE
diff --git a/tests/i/multifile-uncaught/notes.md b/tests/i/multifile-uncaught/notes.md
new file mode 100644
index 0000000..f89adf8
--- /dev/null
+++ b/tests/i/multifile-uncaught/notes.md
@@ -0,0 +1,5 @@
+# Multi-file regression
+
+Human review: pending. Expected output was independently traced by AI.
+
+These are compiler extensions, not Cambridge guide requirements. Fail throws in the library, unwinds the explicit MAIN function and reaches the generated wrapper handler. Exactly the generic message is printed and the process returns 1.
diff --git a/tests/i/multifile-uncaught/program.cps b/tests/i/multifile-uncaught/program.cps
new file mode 100644
index 0000000..d61ea3b
--- /dev/null
+++ b/tests/i/multifile-uncaught/program.cps
@@ -0,0 +1,5 @@
+INCLUDE "lib.cps"
+FUNCTION MAIN RETURNS INTEGER
+ CALL Fail()
+ RETURN 0
+ENDFUNCTION
diff --git a/tests/iii/multifile-body-location/case.json b/tests/iii/multifile-body-location/case.json
new file mode 100644
index 0000000..510015f
--- /dev/null
+++ b/tests/iii/multifile-body-location/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Unknown variable name Missing",
+ "line": 2,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-body-location/lib.cps b/tests/iii/multifile-body-location/lib.cps
new file mode 100644
index 0000000..4afdc98
--- /dev/null
+++ b/tests/iii/multifile-body-location/lib.cps
@@ -0,0 +1,3 @@
+FUNCTION Broken RETURNS INTEGER
+RETURN Missing
+ENDFUNCTION
diff --git a/tests/iii/multifile-body-location/program.cps b/tests/iii/multifile-body-location/program.cps
new file mode 100644
index 0000000..cbca736
--- /dev/null
+++ b/tests/iii/multifile-body-location/program.cps
@@ -0,0 +1,2 @@
+INCLUDE "lib.cps"
+OUTPUT Broken()
diff --git a/tests/iii/multifile-duplicate-constant/case.json b/tests/iii/multifile-duplicate-constant/case.json
new file mode 100644
index 0000000..2ef6783
--- /dev/null
+++ b/tests/iii/multifile-duplicate-constant/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Duplicate definition answer",
+ "line": 1,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-duplicate-constant/lib.cps b/tests/iii/multifile-duplicate-constant/lib.cps
new file mode 100644
index 0000000..fbce689
--- /dev/null
+++ b/tests/iii/multifile-duplicate-constant/lib.cps
@@ -0,0 +1 @@
+CONSTANT answer = 2
diff --git a/tests/iii/multifile-duplicate-constant/program.cps b/tests/iii/multifile-duplicate-constant/program.cps
new file mode 100644
index 0000000..4f49665
--- /dev/null
+++ b/tests/iii/multifile-duplicate-constant/program.cps
@@ -0,0 +1,2 @@
+CONSTANT Answer = 1
+OUTPUT Answer
diff --git a/tests/iii/multifile-duplicate-function/case.json b/tests/iii/multifile-duplicate-function/case.json
new file mode 100644
index 0000000..0d0affd
--- /dev/null
+++ b/tests/iii/multifile-duplicate-function/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Duplicate definition value",
+ "line": 1,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-duplicate-function/lib.cps b/tests/iii/multifile-duplicate-function/lib.cps
new file mode 100644
index 0000000..7fcd8f2
--- /dev/null
+++ b/tests/iii/multifile-duplicate-function/lib.cps
@@ -0,0 +1,3 @@
+FUNCTION value RETURNS INTEGER
+RETURN 2
+ENDFUNCTION
diff --git a/tests/iii/multifile-duplicate-function/program.cps b/tests/iii/multifile-duplicate-function/program.cps
new file mode 100644
index 0000000..8488a19
--- /dev/null
+++ b/tests/iii/multifile-duplicate-function/program.cps
@@ -0,0 +1,4 @@
+OUTPUT 1
+FUNCTION Value RETURNS INTEGER
+RETURN 1
+ENDFUNCTION
diff --git a/tests/iii/multifile-global-array/case.json b/tests/iii/multifile-global-array/case.json
new file mode 100644
index 0000000..e4ec559
--- /dev/null
+++ b/tests/iii/multifile-global-array/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "GLOBAL blocks allow only primitive DECLARE",
+ "line": 2,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-global-array/lib.cps b/tests/iii/multifile-global-array/lib.cps
new file mode 100644
index 0000000..ade1e72
--- /dev/null
+++ b/tests/iii/multifile-global-array/lib.cps
@@ -0,0 +1,3 @@
+GLOBAL
+DECLARE Items : ARRAY[1:2] OF INTEGER
+ENDGLOBAL
diff --git a/tests/iii/multifile-global-array/program.cps b/tests/iii/multifile-global-array/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-global-array/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-global-call/case.json b/tests/iii/multifile-global-call/case.json
new file mode 100644
index 0000000..d11e454
--- /dev/null
+++ b/tests/iii/multifile-global-call/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "GLOBAL initializer must be a literal",
+ "line": 3,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-global-call/lib.cps b/tests/iii/multifile-global-call/lib.cps
new file mode 100644
index 0000000..dae2078
--- /dev/null
+++ b/tests/iii/multifile-global-call/lib.cps
@@ -0,0 +1,7 @@
+GLOBAL
+DECLARE Value : INTEGER
+Value <- Produce()
+ENDGLOBAL
+FUNCTION Produce RETURNS INTEGER
+RETURN 3
+ENDFUNCTION
diff --git a/tests/iii/multifile-global-call/program.cps b/tests/iii/multifile-global-call/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-global-call/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-global-expression/case.json b/tests/iii/multifile-global-expression/case.json
new file mode 100644
index 0000000..d11e454
--- /dev/null
+++ b/tests/iii/multifile-global-expression/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "GLOBAL initializer must be a literal",
+ "line": 3,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-global-expression/lib.cps b/tests/iii/multifile-global-expression/lib.cps
new file mode 100644
index 0000000..dfb6060
--- /dev/null
+++ b/tests/iii/multifile-global-expression/lib.cps
@@ -0,0 +1,4 @@
+GLOBAL
+DECLARE Value : INTEGER
+Value <- 1 + 2
+ENDGLOBAL
diff --git a/tests/iii/multifile-global-expression/program.cps b/tests/iii/multifile-global-expression/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-global-expression/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-hidden-symbols/case.json b/tests/iii/multifile-hidden-symbols/case.json
new file mode 100644
index 0000000..2495840
--- /dev/null
+++ b/tests/iii/multifile-hidden-symbols/case.json
@@ -0,0 +1,28 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Call to undefined function HiddenFunction",
+ "line": 1,
+ "file": "program.cps"
+ },
+ {
+ "reason": "Unknown variable name HiddenVariable",
+ "line": 2,
+ "file": "program.cps"
+ },
+ {
+ "reason": "Unknown variable name HiddenConstant",
+ "line": 3,
+ "file": "program.cps"
+ },
+ {
+ "reason": "Unknown type HiddenType",
+ "line": 4,
+ "file": "program.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-hidden-symbols/lib.cps b/tests/iii/multifile-hidden-symbols/lib.cps
new file mode 100644
index 0000000..aeaa0a3
--- /dev/null
+++ b/tests/iii/multifile-hidden-symbols/lib.cps
@@ -0,0 +1,8 @@
+FUNCTION HiddenFunction RETURNS INTEGER
+RETURN 1
+ENDFUNCTION
+GLOBAL
+DECLARE HiddenVariable : INTEGER
+ENDGLOBAL
+CONSTANT HiddenConstant = 1
+TYPE HiddenType = (HiddenEnum)
diff --git a/tests/iii/multifile-hidden-symbols/program.cps b/tests/iii/multifile-hidden-symbols/program.cps
new file mode 100644
index 0000000..6f926cb
--- /dev/null
+++ b/tests/iii/multifile-hidden-symbols/program.cps
@@ -0,0 +1,4 @@
+OUTPUT HiddenFunction()
+OUTPUT HiddenVariable
+OUTPUT HiddenConstant
+DECLARE Item : HiddenType
diff --git a/tests/iii/multifile-invalid-main/case.json b/tests/iii/multifile-invalid-main/case.json
new file mode 100644
index 0000000..ab929a9
--- /dev/null
+++ b/tests/iii/multifile-invalid-main/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "MAIN must be a FUNCTION with no parameters returning INTEGER",
+ "line": 1,
+ "file": "program.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-invalid-main/lib.cps b/tests/iii/multifile-invalid-main/lib.cps
new file mode 100644
index 0000000..af7b016
--- /dev/null
+++ b/tests/iii/multifile-invalid-main/lib.cps
@@ -0,0 +1 @@
+CONSTANT Value = 1
diff --git a/tests/iii/multifile-invalid-main/program.cps b/tests/iii/multifile-invalid-main/program.cps
new file mode 100644
index 0000000..cd8bb37
--- /dev/null
+++ b/tests/iii/multifile-invalid-main/program.cps
@@ -0,0 +1,3 @@
+FUNCTION MAIN(n : INTEGER) RETURNS INTEGER
+RETURN n
+ENDFUNCTION
diff --git a/tests/iii/multifile-lexer-location/case.json b/tests/iii/multifile-lexer-location/case.json
new file mode 100644
index 0000000..5190ab3
--- /dev/null
+++ b/tests/iii/multifile-lexer-location/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Unterminated string literal",
+ "line": 1,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-lexer-location/lib.cps b/tests/iii/multifile-lexer-location/lib.cps
new file mode 100644
index 0000000..a0619d2
--- /dev/null
+++ b/tests/iii/multifile-lexer-location/lib.cps
@@ -0,0 +1 @@
+CONSTANT Broken = "unterminated
diff --git a/tests/iii/multifile-lexer-location/program.cps b/tests/iii/multifile-lexer-location/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-lexer-location/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-main-and-code/case.json b/tests/iii/multifile-main-and-code/case.json
new file mode 100644
index 0000000..9e770f6
--- /dev/null
+++ b/tests/iii/multifile-main-and-code/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "cannot coexist with top-level code",
+ "line": 1,
+ "file": "program.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-main-and-code/lib.cps b/tests/iii/multifile-main-and-code/lib.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-main-and-code/lib.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-main-and-code/program.cps b/tests/iii/multifile-main-and-code/program.cps
new file mode 100644
index 0000000..90ddf90
--- /dev/null
+++ b/tests/iii/multifile-main-and-code/program.cps
@@ -0,0 +1,3 @@
+FUNCTION MAIN RETURNS INTEGER
+RETURN 0
+ENDFUNCTION
diff --git a/tests/iii/multifile-missing-include/case.json b/tests/iii/multifile-missing-include/case.json
new file mode 100644
index 0000000..f5b32c8
--- /dev/null
+++ b/tests/iii/multifile-missing-include/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "cannot INCLUDE 'missing.cps': not given on the command line",
+ "line": 1,
+ "file": "program.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-missing-include/lib.cps b/tests/iii/multifile-missing-include/lib.cps
new file mode 100644
index 0000000..af7b016
--- /dev/null
+++ b/tests/iii/multifile-missing-include/lib.cps
@@ -0,0 +1 @@
+CONSTANT Value = 1
diff --git a/tests/iii/multifile-missing-include/program.cps b/tests/iii/multifile-missing-include/program.cps
new file mode 100644
index 0000000..772f3f2
--- /dev/null
+++ b/tests/iii/multifile-missing-include/program.cps
@@ -0,0 +1,2 @@
+INCLUDE "missing.cps"
+OUTPUT 1
diff --git a/tests/iii/multifile-no-entry/case.json b/tests/iii/multifile-no-entry/case.json
new file mode 100644
index 0000000..f765fda
--- /dev/null
+++ b/tests/iii/multifile-no-entry/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "no main program",
+ "line": 1,
+ "file": "program.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-no-entry/lib.cps b/tests/iii/multifile-no-entry/lib.cps
new file mode 100644
index 0000000..e167bd1
--- /dev/null
+++ b/tests/iii/multifile-no-entry/lib.cps
@@ -0,0 +1,2 @@
+PROCEDURE Work()
+ENDPROCEDURE
diff --git a/tests/iii/multifile-no-entry/program.cps b/tests/iii/multifile-no-entry/program.cps
new file mode 100644
index 0000000..af7b016
--- /dev/null
+++ b/tests/iii/multifile-no-entry/program.cps
@@ -0,0 +1 @@
+CONSTANT Value = 1
diff --git a/tests/iii/multifile-parser-location/case.json b/tests/iii/multifile-parser-location/case.json
new file mode 100644
index 0000000..698d9d0
--- /dev/null
+++ b/tests/iii/multifile-parser-location/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "INCLUDE is only allowed at top level",
+ "line": 2,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-parser-location/lib.cps b/tests/iii/multifile-parser-location/lib.cps
new file mode 100644
index 0000000..81ef7e3
--- /dev/null
+++ b/tests/iii/multifile-parser-location/lib.cps
@@ -0,0 +1,3 @@
+PROCEDURE Broken()
+INCLUDE "program.cps"
+ENDPROCEDURE
diff --git a/tests/iii/multifile-parser-location/program.cps b/tests/iii/multifile-parser-location/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-parser-location/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/iii/multifile-two-entries/case.json b/tests/iii/multifile-two-entries/case.json
new file mode 100644
index 0000000..8895fad
--- /dev/null
+++ b/tests/iii/multifile-two-entries/case.json
@@ -0,0 +1,13 @@
+{
+ "sources": [
+ "program.cps",
+ "lib.cps"
+ ],
+ "errors": [
+ {
+ "reason": "Top-level code in both",
+ "line": 1,
+ "file": "lib.cps"
+ }
+ ]
+}
diff --git a/tests/iii/multifile-two-entries/lib.cps b/tests/iii/multifile-two-entries/lib.cps
new file mode 100644
index 0000000..b196807
--- /dev/null
+++ b/tests/iii/multifile-two-entries/lib.cps
@@ -0,0 +1 @@
+DECLARE Local : INTEGER
diff --git a/tests/iii/multifile-two-entries/program.cps b/tests/iii/multifile-two-entries/program.cps
new file mode 100644
index 0000000..5fd1059
--- /dev/null
+++ b/tests/iii/multifile-two-entries/program.cps
@@ -0,0 +1 @@
+OUTPUT 1
diff --git a/tests/run.pl b/tests/run.pl
index 41437de..54cd10c 100644
--- a/tests/run.pl
+++ b/tests/run.pl
@@ -113,13 +113,16 @@ sub check_diagnostics {
my @matching = grep { /error:/ && /$reason/ } @lines;
my $found = 0;
for my $line (@matching) {
- if ($line =~ /\bline $line_no\b/) {
+ my $file = $expectation->{file};
+ if ($line =~ /\bline $line_no\b/ &&
+ (!defined($file) || $line =~ /\bline $line_no in \Q$file\E(?:\s|$)/)) {
$found = 1;
last;
}
}
if (!$found) {
push @errors, 'missing error /' . $reason . '/ at line ' . $line_no
+ . (defined($expectation->{file}) ? ' in ' . $expectation->{file} : '')
. '; matching diagnostics: [' . join(', ', map { show_bytes($_) } @matching) . ']';
}
}
@@ -339,7 +342,15 @@ sub main {
run_mode => $opt_run_mode,
failures => [],
};
- my $source = File::Spec->catfile($case_dir, 'program.cps');
+ my @sources = map { File::Spec->catfile($case_dir, split(m{/}, $_)) }
+ @{$config->{sources} // ['program.cps']};
+ my @expectations = map {
+ my %expectation = %{$_};
+ if (defined($expectation{file})) {
+ $expectation{file} = File::Spec->catfile($case_dir, split(m{/}, $expectation{file}));
+ }
+ \%expectation;
+ } @{$config->{errors} // []};
my $attempt = sub {
my ($label, $code) = @_;
@@ -367,7 +378,7 @@ sub main {
write_file($target, $sentinel);
}
my $result = run_command($commands,
- [$cpsc_abs, $source, @flags, '-o', $target], $work, '', 120);
+ [$cpsc_abs, @sources, @flags, '-o', $target], $work, '', 120);
my $preservation_error;
if ($existing && (!-f $target || read_file($target) ne $sentinel)) {
$preservation_error = 'compilation failure overwrote existing output';
@@ -384,7 +395,7 @@ sub main {
if (defined($captured_error)) {
die "$captured_error\n";
}
- check_diagnostics($captured_result, $config->{errors});
+ check_diagnostics($captured_result, \@expectations);
},
);
}
@@ -443,12 +454,12 @@ sub main {
my $target = File::Spec->catfile($work,
$IS_WINDOWS ? 'program.exe' : 'program.bin');
my $compiled = run_command($commands,
- [$cpsc_abs, $source, '-o', $target], $work, '', 120);
+ [$cpsc_abs, @sources, '-o', $target], $work, '', 120);
check_result($compiled, '');
$command = [$target];
}
else {
- $command = [$cpsc_abs, $source, '-r'];
+ $command = [$cpsc_abs, @sources, '-r'];
}
my $exec_timeout = exists($config->{timeout}) ? $config->{timeout} + 0 : 30;
my $result = run_command($commands, $command, $work, $data, $exec_timeout);
diff --git a/tests/test_runner.pl b/tests/test_runner.pl
index af08556..16a0e35 100644
--- a/tests/test_runner.pl
+++ b/tests/test_runner.pl
@@ -52,6 +52,23 @@ for my $bad (
ok(!$ok, 'diagnostic mismatch rejected: ' . show_bytes($bad));
}
+my $file_expectation = [{reason => "Expected ':'", line => 3, file => '/fixtures/lib.cps'}];
+for my $sample (
+ ["error: Expected ':' at line 3 in /fixtures/lib.cps", 1],
+ ["error: Expected ':' at line 3 in /fixtures/program.cps", 0],
+ ["error: Expected ':' at line 3 in /fixtures/lib.cps.extra", 0],
+ ["error: Expected ':' at line 30 in /fixtures/lib.cps", 0],
+ ["error: Expected ':' at line 3\nerror: unrelated at line 3 in /fixtures/lib.cps", 0],
+) {
+ my ($diagnostic, $expected) = @{$sample};
+ my $ok = eval {
+ check_diagnostics({returncode => 1, stdout => '',
+ stderr => $diagnostic . "\ncompilation terminated.\n"}, $file_expectation);
+ 1;
+ };
+ is($ok ? 1 : 0, $expected, 'file-qualified diagnostic: ' . show_bytes($diagnostic));
+}
+
my $dir = tempdir(CLEANUP => 1);
my $state = commands_new($dir);
my $ran = eval { run_command($state, [$^X, '-e', 'sleep 5'], $dir, '', 1); 1; };