aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.one>2026-10-16 20:53:55 +0800
committer_ov4 <ov4@ov4.one>2026-10-16 20:53:55 +0800
commitd19f9ddb866ac2415cbfe66966897a8efe8ddb63 (patch)
tree6be1a9a6e316e776a396813f7cb22c688287b21a
parent5c00b8cd03cbfc516cce92229f3f4974d7549c24 (diff)
test(compiler): add seeded C++ differential tests across seven groups
Generate 10000 random records per group plus boundary inputs using a portable SHA-256 stream. Compare native and runner output independently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-rw-r--r--tests/CMakeLists.txt2
-rw-r--r--tests/ii/Random.pm25
-rw-r--r--tests/ii/arithmetic/case.json1
-rw-r--r--tests/ii/arithmetic/generate.pl23
-rw-r--r--tests/ii/arithmetic/notes.md5
-rw-r--r--tests/ii/arithmetic/program.cps12
-rw-r--r--tests/ii/arithmetic/reference.cc13
-rw-r--r--tests/ii/arrays/case.json1
-rw-r--r--tests/ii/arrays/generate.pl17
-rw-r--r--tests/ii/arrays/notes.md5
-rw-r--r--tests/ii/arrays/program.cps22
-rw-r--r--tests/ii/arrays/reference.cc13
-rw-r--r--tests/ii/calls/case.json1
-rw-r--r--tests/ii/calls/generate.pl16
-rw-r--r--tests/ii/calls/notes.md5
-rw-r--r--tests/ii/calls/program.cps40
-rw-r--r--tests/ii/calls/reference.cc12
-rw-r--r--tests/ii/files/case.json3
-rw-r--r--tests/ii/files/generate.pl21
-rw-r--r--tests/ii/files/notes.md5
-rw-r--r--tests/ii/files/program.cps29
-rw-r--r--tests/ii/files/reference.cc20
-rw-r--r--tests/ii/native-types/case.json1
-rw-r--r--tests/ii/native-types/generate.pl20
-rw-r--r--tests/ii/native-types/notes.md5
-rw-r--r--tests/ii/native-types/program.cps22
-rw-r--r--tests/ii/native-types/reference.cc18
-rw-r--r--tests/ii/real/case.json1
-rw-r--r--tests/ii/real/generate.pl17
-rw-r--r--tests/ii/real/notes.md5
-rw-r--r--tests/ii/real/program.cps17
-rw-r--r--tests/ii/real/reference.cc15
-rw-r--r--tests/ii/strings/case.json1
-rw-r--r--tests/ii/strings/generate.pl23
-rw-r--r--tests/ii/strings/notes.md5
-rw-r--r--tests/ii/strings/program.cps17
-rw-r--r--tests/ii/strings/reference.cc20
-rw-r--r--tests/run.py5
38 files changed, 480 insertions, 3 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c5a7dc2..4d385b9 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -23,6 +23,6 @@ foreach(suite i ii iii)
--seed "${CPSC_TEST_SEED}" --count "${CPSC_TEST_COUNT}"
--results "${CMAKE_BINARY_DIR}/test-results" ${reference_args})
set_tests_properties("${suite}/${case_name}" PROPERTIES
- LABELS "${suite}" TIMEOUT 600)
+ LABELS "${suite}" TIMEOUT 900)
endforeach()
endforeach()
diff --git a/tests/ii/Random.pm b/tests/ii/Random.pm
new file mode 100644
index 0000000..a7a95e5
--- /dev/null
+++ b/tests/ii/Random.pm
@@ -0,0 +1,25 @@
+package Random;
+use strict;
+use warnings;
+use Digest::SHA qw(sha256);
+
+# Fixed byte order and decimal counter: independent of Perl's rand and hash seed.
+sub new {
+ my ($class, $seed, $group) = @_;
+ return bless { seed => $seed, group => $group, counter => 0 }, $class;
+}
+sub integer {
+ my ($self, $low, $high) = @_;
+ die "invalid random range\n" if $high < $low || $high - $low >= 4294967296;
+ my $bytes = sha256($self->{seed} . "\0" . $self->{group} . "\0" . $self->{counter}++);
+ return $low + unpack('N', substr($bytes, 0, 4)) % ($high - $low + 1);
+}
+sub emit {
+ my ($self, $count, $edges, $make) = @_;
+ die "positive count required\n" unless defined($count) && $count =~ /^[1-9][0-9]*$/;
+ binmode STDOUT; # Identical input bytes on every platform.
+ print scalar(@$edges) + $count, "\n";
+ print "$_\n" for @$edges;
+ print $make->(), "\n" for 1 .. $count;
+}
+1;
diff --git a/tests/ii/arithmetic/case.json b/tests/ii/arithmetic/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/arithmetic/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/arithmetic/generate.pl b/tests/ii/arithmetic/generate.pl
new file mode 100644
index 0000000..24236a8
--- /dev/null
+++ b/tests/ii/arithmetic/generate.pl
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "arithmetic");
+my @edges = (
+ '9223372036854775807 1',
+ '-9223372036854775808 1',
+ '-9223372036854775808 2',
+ '0 -1',
+ '7 -3',
+ '-7 3',
+ '-7 -3',
+ '1 1',
+);
+$rng->emit($count, \@edges, sub {
+my $a = $rng->integer(-1000000, 1000000);
+my $b = $rng->integer(-1000000, 1000000);
+$b = 1 if $b == 0;
+return "$a $b";
+});
diff --git a/tests/ii/arithmetic/notes.md b/tests/ii/arithmetic/notes.md
new file mode 100644
index 0000000..947a3fa
--- /dev/null
+++ b/tests/ii/arithmetic/notes.md
@@ -0,0 +1,5 @@
+# arithmetic
+
+CIE pp. 15-16, project i64 and truncation-towards-zero DIV/MOD. Full-width extremes are used only in safe division/remainder and comparisons; addition, subtraction and multiplication use A MOD 1000001. B is nonzero, and INT64_MIN / -1 is never generated.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/arithmetic/program.cps b/tests/ii/arithmetic/program.cps
new file mode 100644
index 0000000..7c4bf52
--- /dev/null
+++ b/tests/ii/arithmetic/program.cps
@@ -0,0 +1,12 @@
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE A : INTEGER
+DECLARE B : INTEGER
+DECLARE Small : INTEGER
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT A
+ INPUT B
+ Small <- A MOD 1000001
+ OUTPUT A < B, ":", A DIV B, ":", A MOD B, ":", Small + B, ":", Small - B, ":", Small * B
+NEXT Index
diff --git a/tests/ii/arithmetic/reference.cc b/tests/ii/arithmetic/reference.cc
new file mode 100644
index 0000000..6d3d98f
--- /dev/null
+++ b/tests/ii/arithmetic/reference.cc
@@ -0,0 +1,13 @@
+#include <cstdint>
+#include <iostream>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ std::int64_t a, b;
+ if (!(std::cin >> a >> b) || b == 0) return 1;
+ auto small = a % 1000001;
+ std::cout << (a < b ? "TRUE" : "FALSE") << ':' << a / b << ':' << a % b
+ << ':' << small + b << ':' << small - b << ':' << small * b << '\n';
+ }
+}
diff --git a/tests/ii/arrays/case.json b/tests/ii/arrays/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/arrays/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/arrays/generate.pl b/tests/ii/arrays/generate.pl
new file mode 100644
index 0000000..6f4a5bd
--- /dev/null
+++ b/tests/ii/arrays/generate.pl
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "arrays");
+my @edges = (
+ '-20 1 0 0',
+ '0 12 -100 1000',
+ '1 2 1 -1',
+ '-5 12 100 -1000',
+);
+$rng->emit($count, \@edges, sub {
+return join ' ', $rng->integer(-20,20), $rng->integer(1,12),
+ $rng->integer(-100,100), $rng->integer(-1000,1000);
+});
diff --git a/tests/ii/arrays/notes.md b/tests/ii/arrays/notes.md
new file mode 100644
index 0000000..19300bf
--- /dev/null
+++ b/tests/ii/arrays/notes.md
@@ -0,0 +1,5 @@
+# arrays
+
+CIE pp. 10-11,20; project dynamic array bounds. The C++ oracle uses an arithmetic-series formula, not the pseudocode loop. Sizes stay 1..12 and both inclusive endpoints are observed.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/arrays/program.cps b/tests/ii/arrays/program.cps
new file mode 100644
index 0000000..5d2562e
--- /dev/null
+++ b/tests/ii/arrays/program.cps
@@ -0,0 +1,22 @@
+DECLARE Count : INTEGER
+DECLARE Record : INTEGER
+DECLARE Lower : INTEGER
+DECLARE Size : INTEGER
+DECLARE Step : INTEGER
+DECLARE Base : INTEGER
+DECLARE I : INTEGER
+DECLARE Sum : INTEGER
+INPUT Count
+FOR Record <- 1 TO Count
+ INPUT Lower
+ INPUT Size
+ INPUT Step
+ INPUT Base
+ DECLARE Values : ARRAY[Lower:Lower+Size-1] OF INTEGER
+ Sum <- 0
+ FOR I <- Lower TO Lower + Size - 1
+ Values[I] <- Base + (I - Lower) * Step
+ Sum <- Sum + Values[I]
+ NEXT I
+ OUTPUT Sum, ":", Values[Lower], ":", Values[Lower+Size-1]
+NEXT Record
diff --git a/tests/ii/arrays/reference.cc b/tests/ii/arrays/reference.cc
new file mode 100644
index 0000000..9a136bd
--- /dev/null
+++ b/tests/ii/arrays/reference.cc
@@ -0,0 +1,13 @@
+#include <cstdint>
+#include <iostream>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int record = 0; record < count; ++record) {
+ std::int64_t lower, size, step, base;
+ if (!(std::cin >> lower >> size >> step >> base) || size < 1) return 1;
+ // Closed form is independent of pseudocode's indexed loop.
+ auto sum = size * base + step * size * (size - 1) / 2;
+ std::cout << sum << ':' << base << ':' << base + (size - 1) * step << '\n';
+ }
+}
diff --git a/tests/ii/calls/case.json b/tests/ii/calls/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/calls/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/calls/generate.pl b/tests/ii/calls/generate.pl
new file mode 100644
index 0000000..8bfe996
--- /dev/null
+++ b/tests/ii/calls/generate.pl
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "calls");
+my @edges = (
+ '0 0 0',
+ '1 -1000000 1000000',
+ '10 1000000 -1000000',
+);
+$rng->emit($count, \@edges, sub {
+return join ' ', $rng->integer(0,10), $rng->integer(-1000000,1000000),
+ $rng->integer(-1000000,1000000);
+});
diff --git a/tests/ii/calls/notes.md b/tests/ii/calls/notes.md
new file mode 100644
index 0000000..87c999d
--- /dev/null
+++ b/tests/ii/calls/notes.md
@@ -0,0 +1,5 @@
+# calls
+
+CIE pp. 12-14,22-24. Exercises record copies/BYVAL, pointer field mutation, sticky BYREF scalar parameters and recursive return paths. The oracle calculates direct values and a closed-form triangular sum.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/calls/program.cps b/tests/ii/calls/program.cps
new file mode 100644
index 0000000..9a79ad6
--- /dev/null
+++ b/tests/ii/calls/program.cps
@@ -0,0 +1,40 @@
+TYPE Pair
+ DECLARE X : INTEGER
+ DECLARE Y : INTEGER
+ENDTYPE
+TYPE PairPtr = ^Pair
+PROCEDURE IgnoreCopy(P : Pair)
+ P.X <- 999
+ENDPROCEDURE
+PROCEDURE Shift(BYREF X : INTEGER, Y : INTEGER)
+ X <- X + 1
+ Y <- Y - 1
+ENDPROCEDURE
+FUNCTION Triangle(N : INTEGER) RETURNS INTEGER
+ IF N = 0 THEN
+ RETURN 0
+ ENDIF
+ RETURN N + Triangle(N - 1)
+ENDFUNCTION
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE N : INTEGER
+DECLARE Value : Pair
+DECLARE Saved : Pair
+DECLARE P : PairPtr
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT N
+ DECLARE X : INTEGER
+ DECLARE Y : INTEGER
+ INPUT X
+ INPUT Y
+ Value.X <- X
+ Value.Y <- Y
+ Saved <- Value
+ CALL IgnoreCopy(Value)
+ P <- ^Value
+ P^.X <- P^.X + P^.Y
+ CALL Shift(X, Y)
+ OUTPUT Value.X, ":", Saved.X, ":", X, ":", Y, ":", Triangle(N)
+NEXT Index
diff --git a/tests/ii/calls/reference.cc b/tests/ii/calls/reference.cc
new file mode 100644
index 0000000..c5ca6ca
--- /dev/null
+++ b/tests/ii/calls/reference.cc
@@ -0,0 +1,12 @@
+#include <cstdint>
+#include <iostream>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ std::int64_t n, x, y;
+ if (!(std::cin >> n >> x >> y)) return 1;
+ std::cout << x + y << ':' << x << ':' << x + 1 << ':' << y - 1
+ << ':' << n * (n + 1) / 2 << '\n';
+ }
+}
diff --git a/tests/ii/files/case.json b/tests/ii/files/case.json
new file mode 100644
index 0000000..5609dd2
--- /dev/null
+++ b/tests/ii/files/case.json
@@ -0,0 +1,3 @@
+{
+ "timeout": 180
+}
diff --git a/tests/ii/files/generate.pl b/tests/ii/files/generate.pl
new file mode 100644
index 0000000..18020d0
--- /dev/null
+++ b/tests/ii/files/generate.pl
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "files");
+my @edges = (
+ 'first last',
+ 'A Z',
+ '0 0',
+);
+$rng->emit($count, \@edges, sub {
+my @alphabet = split //, 'ABCabc019_-';
+my @words;
+for (1..2) {
+ my $size = $rng->integer(1,24);
+ push @words, join '', map { $alphabet[$rng->integer(0,$#alphabet)] } 1 .. $size;
+}
+return join ' ', @words;
+});
diff --git a/tests/ii/files/notes.md b/tests/ii/files/notes.md
new file mode 100644
index 0000000..02831fa
--- /dev/null
+++ b/tests/ii/files/notes.md
@@ -0,0 +1,5 @@
+# files
+
+CIE p. 25. Both programs exercise actual text files, including truncation, append, empty lines and EOF. Separate working directories avoid shared files. Pseudocode uses the project-required WHILE DO spelling; i/standard-while separately checks the standard spelling.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/files/program.cps b/tests/ii/files/program.cps
new file mode 100644
index 0000000..9bc4aad
--- /dev/null
+++ b/tests/ii/files/program.cps
@@ -0,0 +1,29 @@
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE Text : STRING
+DECLARE Extra : STRING
+DECLARE Line : STRING
+DECLARE Combined : STRING
+DECLARE Lines : INTEGER
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT Text
+ INPUT Extra
+ OPENFILE "roundtrip.txt" FOR WRITE
+ WRITEFILE "roundtrip.txt", Text
+ WRITEFILE "roundtrip.txt", ""
+ CLOSEFILE "roundtrip.txt"
+ OPENFILE "roundtrip.txt" FOR APPEND
+ WRITEFILE "roundtrip.txt", Extra
+ CLOSEFILE "roundtrip.txt"
+ OPENFILE "roundtrip.txt" FOR READ
+ Combined <- ""
+ Lines <- 0
+ WHILE NOT EOF("roundtrip.txt") DO
+ READFILE "roundtrip.txt", Line
+ Combined <- Combined & "[" & Line & "]"
+ Lines <- Lines + 1
+ ENDWHILE
+ CLOSEFILE "roundtrip.txt"
+ OUTPUT Lines, ":", Combined
+NEXT Index
diff --git a/tests/ii/files/reference.cc b/tests/ii/files/reference.cc
new file mode 100644
index 0000000..d9b784c
--- /dev/null
+++ b/tests/ii/files/reference.cc
@@ -0,0 +1,20 @@
+#include <fstream>
+#include <iostream>
+#include <string>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ std::string text, extra;
+ if (!(std::cin >> text >> extra)) return 1;
+ { std::ofstream file("roundtrip.txt"); file << text << "\n\n"; if (!file) return 1; }
+ { std::ofstream file("roundtrip.txt", std::ios::app); file << extra << '\n'; if (!file) return 1; }
+ std::ifstream file("roundtrip.txt");
+ if (!file) return 1;
+ std::string line, combined;
+ int lines = 0;
+ while (std::getline(file, line)) { combined += '[' + line + ']'; ++lines; }
+ if (!file.eof()) return 1;
+ std::cout << lines << ':' << combined << '\n';
+ }
+}
diff --git a/tests/ii/native-types/case.json b/tests/ii/native-types/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/native-types/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/native-types/generate.pl b/tests/ii/native-types/generate.pl
new file mode 100644
index 0000000..35332cf
--- /dev/null
+++ b/tests/ii/native-types/generate.pl
@@ -0,0 +1,20 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "native-types");
+my @edges = (
+ '0 0',
+ '2147483647 127',
+ '2147483648 128',
+ '4294967295 255',
+ '4294967296 0',
+ '9223372036854775807 255',
+ '-9223372036854775808 128',
+ '-1 255',
+);
+$rng->emit($count, \@edges, sub {
+return join ' ', $rng->integer(-1000000,1000000), $rng->integer(0,255);
+});
diff --git a/tests/ii/native-types/notes.md b/tests/ii/native-types/notes.md
new file mode 100644
index 0000000..a9de803
--- /dev/null
+++ b/tests/ii/native-types/notes.md
@@ -0,0 +1,5 @@
+# native-types
+
+Project C_TYPE contract only. C++ is built with the same CMake toolchain and ABI as cpsc: native long width, plain-char signedness, integer promotions and unsigned wrap follow that platform. Signed out-of-range casts are compared to that native implementation; no signed arithmetic overflow is performed. No pseudocode INTEGER overflow or REAL output is involved.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/native-types/program.cps b/tests/ii/native-types/program.cps
new file mode 100644
index 0000000..92967b7
--- /dev/null
+++ b/tests/ii/native-types/program.cps
@@ -0,0 +1,22 @@
+TYPE NativeLong = C_TYPE("long")
+TYPE NativeChar = C_TYPE("char")
+TYPE Byte = C_TYPE("unsigned char")
+TYPE Word = C_TYPE("unsigned int")
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE A : INTEGER
+DECLARE B : INTEGER
+DECLARE L : NativeLong
+DECLARE C : NativeChar
+DECLARE U : Byte
+DECLARE W : Word
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT A
+ INPUT B
+ L <- (NativeLong) A
+ C <- (NativeChar) B
+ U <- (Byte) B
+ W <- (Word) A
+ OUTPUT L, ":", (INTEGER) C, ":", U + (Byte) 1, ":", W + (Word) 1
+NEXT Index
diff --git a/tests/ii/native-types/reference.cc b/tests/ii/native-types/reference.cc
new file mode 100644
index 0000000..64fc4cc
--- /dev/null
+++ b/tests/ii/native-types/reference.cc
@@ -0,0 +1,18 @@
+#include <cstdint>
+#include <iostream>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ std::int64_t a;
+ int b;
+ if (!(std::cin >> a >> b)) return 1;
+ auto l = static_cast<long>(a);
+ auto c = static_cast<char>(b);
+ auto u = static_cast<unsigned char>(b);
+ auto w = static_cast<unsigned int>(a);
+ std::cout << l << ':' << static_cast<std::int64_t>(c) << ':'
+ << u + static_cast<unsigned char>(1) << ':'
+ << w + static_cast<unsigned int>(1) << '\n';
+ }
+}
diff --git a/tests/ii/real/case.json b/tests/ii/real/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/real/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/real/generate.pl b/tests/ii/real/generate.pl
new file mode 100644
index 0000000..cf11ce6
--- /dev/null
+++ b/tests/ii/real/generate.pl
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "real");
+my @edges = (
+ '0 0',
+ '-8192 8192',
+ '8192 -8192',
+ '1 1',
+ '-1 1',
+);
+$rng->emit($count, \@edges, sub {
+return join ' ', $rng->integer(-8192,8192), $rng->integer(-8192,8192);
+});
diff --git a/tests/ii/real/notes.md b/tests/ii/real/notes.md
new file mode 100644
index 0000000..59995b2
--- /dev/null
+++ b/tests/ii/real/notes.md
@@ -0,0 +1,5 @@
+# real
+
+CIE pp. 15-16,23-24. Binary fractions with denominator 8 keep sums and products exactly representable, so these checks need neither tolerances nor REAL text formatting. All intermediates fit a 32-bit C++ int.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/real/program.cps b/tests/ii/real/program.cps
new file mode 100644
index 0000000..57834e1
--- /dev/null
+++ b/tests/ii/real/program.cps
@@ -0,0 +1,17 @@
+FUNCTION Twice(Value : REAL) RETURNS REAL
+ RETURN Value * 2.0
+ENDFUNCTION
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE A : INTEGER
+DECLARE B : INTEGER
+DECLARE X : REAL
+DECLARE Y : REAL
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT A
+ INPUT B
+ X <- A / 8
+ Y <- B / 8
+ OUTPUT X < Y, ":", X = Y, ":", Twice(X) = (A / 4), ":", (X + Y) = ((A + B) / 8), ":", (X * Y) = ((A * B) / 64)
+NEXT Index
diff --git a/tests/ii/real/reference.cc b/tests/ii/real/reference.cc
new file mode 100644
index 0000000..4c1e118
--- /dev/null
+++ b/tests/ii/real/reference.cc
@@ -0,0 +1,15 @@
+#include <iostream>
+const char *boolean(bool value) { return value ? "TRUE" : "FALSE"; }
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ int a, b;
+ if (!(std::cin >> a >> b)) return 1;
+ double x = a / 8.0, y = b / 8.0;
+ std::cout << boolean(x < y) << ':' << boolean(x == y) << ':'
+ << boolean(2.0 * x == a / 4.0) << ':'
+ << boolean(x + y == (a + b) / 8.0) << ':'
+ << boolean(x * y == (a * b) / 64.0) << '\n';
+ }
+}
diff --git a/tests/ii/strings/case.json b/tests/ii/strings/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/ii/strings/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/ii/strings/generate.pl b/tests/ii/strings/generate.pl
new file mode 100644
index 0000000..f3ac098
--- /dev/null
+++ b/tests/ii/strings/generate.pl
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+use FindBin;
+use lib "$FindBin::Bin/..";
+use Random;
+my ($seed, $count) = @ARGV;
+my $rng = Random->new($seed, "strings");
+my @edges = (
+ '1 placeholder 1 0',
+ '0 A 1 1',
+ '0 Ab09 4 1',
+ '0 Ab09 1 4',
+ '0 Ab09 2 0',
+);
+$rng->emit($count, \@edges, sub {
+my $size = $rng->integer(0,24);
+return '1 placeholder 1 0' if $size == 0;
+my @alphabet = split //, 'ABCXYZabcxyz019@_-';
+my $text = join '', map { $alphabet[$rng->integer(0,$#alphabet)] } 1 .. $size;
+my $start = $rng->integer(1,$size);
+my $width = $rng->integer(0,$size-$start+1);
+return "0 $text $start $width";
+});
diff --git a/tests/ii/strings/notes.md b/tests/ii/strings/notes.md
new file mode 100644
index 0000000..01ea90a
--- /dev/null
+++ b/tests/ii/strings/notes.md
@@ -0,0 +1,5 @@
+# strings
+
+CIE pp. 16-17 for LENGTH/MID/RIGHT/concatenation; full-STRING LCASE/UCASE is a project extension. Inputs are ASCII under the default C locale. Empty input is encoded with a flag because project INPUT reads tokens. Substrings remain in range; zero width is a documented project boundary.
+
+Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded.
diff --git a/tests/ii/strings/program.cps b/tests/ii/strings/program.cps
new file mode 100644
index 0000000..55e7541
--- /dev/null
+++ b/tests/ii/strings/program.cps
@@ -0,0 +1,17 @@
+DECLARE Count : INTEGER
+DECLARE Index : INTEGER
+DECLARE Empty : INTEGER
+DECLARE Text : STRING
+DECLARE Start : INTEGER
+DECLARE Width : INTEGER
+INPUT Count
+FOR Index <- 1 TO Count
+ INPUT Empty
+ INPUT Text
+ INPUT Start
+ INPUT Width
+ IF Empty = 1 THEN
+ Text <- ""
+ ENDIF
+ OUTPUT LENGTH(Text), ":", MID(Text, Start, Width), ":", RIGHT(Text, Width), ":", Text = ("" & Text), ":", LCASE(Text), ":", UCASE(Text)
+NEXT Index
diff --git a/tests/ii/strings/reference.cc b/tests/ii/strings/reference.cc
new file mode 100644
index 0000000..54d6efc
--- /dev/null
+++ b/tests/ii/strings/reference.cc
@@ -0,0 +1,20 @@
+#include <algorithm>
+#include <cctype>
+#include <iostream>
+#include <string>
+int main() {
+ int count;
+ if (!(std::cin >> count)) return 1;
+ for (int i = 0; i < count; ++i) {
+ int empty, start, width;
+ std::string text;
+ if (!(std::cin >> empty >> text >> start >> width)) return 1;
+ if (empty) text.clear();
+ auto lower = text, upper = text;
+ for (auto &c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
+ for (auto &c : upper) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
+ auto mid = text.substr(static_cast<std::size_t>(start - 1), static_cast<std::size_t>(width));
+ auto right = text.substr(text.size() - static_cast<std::size_t>(width));
+ std::cout << text.size() << ':' << mid << ':' << right << ":TRUE:" << lower << ':' << upper << '\n';
+ }
+}
diff --git a/tests/run.py b/tests/run.py
index 432d1b9..0d57728 100644
--- a/tests/run.py
+++ b/tests/run.py
@@ -201,7 +201,8 @@ def main(argv=None):
if not records or int(records[0]) != len(records) - 1:
raise Failure("generator must emit record count followed by one record per line")
work = workspace(case_dir, directory, "reference")
- reference = commands.run([args.reference], work, data)
+ reference = commands.run([args.reference], work, data,
+ timeout=config.get("timeout", 30))
if reference.returncode != 0 or reference.stderr:
raise Failure(f"reference failed ({reference.returncode}): {reference.stderr!r}")
expected = reference.stdout
@@ -221,7 +222,7 @@ def main(argv=None):
command = [target]
else:
command = [args.cpsc, source, "-r"]
- result = commands.run(command, work, data)
+ result = commands.run(command, work, data, timeout=config.get("timeout", 30))
if suite == "ii" and normalized(result.stdout) != normalized(expected):
wanted, got = normalized(expected).splitlines(), normalized(result.stdout).splitlines()
index = next((i for i in range(min(len(wanted), len(got)))