aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS/CONVENTIONS.md2
-rw-r--r--AGENTS/SUBSYSTEMS.md4
-rw-r--r--AGENTS/TESTING.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-class.7.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-delete.7.md2
-rw-r--r--lib/CodeGen/FunctionGen.cc2
-rw-r--r--pseudocode-lib/README.md20
-rw-r--r--pseudocode-lib/_exp_string.cc75
-rw-r--r--pseudocode-lib/_exp_string.cps67
-rw-r--r--tests/CMakeLists.txt14
-rw-r--r--tests/README.md2
-rw-r--r--tests/i/class-copy/notes.md5
-rw-r--r--tests/i/escape-sequences/notes.md5
-rw-r--r--tests/i/extern-cc-runtime/notes.md5
-rw-r--r--tests/i/library-lines/notes.md5
-rw-r--r--tests/i/operator-assign/notes.md5
-rw-r--r--tests/i/underscore-identifiers/notes.md5
-rw-r--r--tests/library/exp-string/case.json1
-rw-r--r--tests/library/exp-string/expected-stderr.txt2
-rw-r--r--tests/library/exp-string/expected.txt10
-rw-r--r--tests/library/exp-string/notes.md5
-rw-r--r--tests/library/exp-string/program.cps41
22 files changed, 275 insertions, 6 deletions
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index f72cb86..76f67c1 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -85,7 +85,7 @@ before ArithmeticHandler. Ordering is case-sensitive byte lexicographic order;
mixed STRING/non-STRING comparisons are errors. Whole-array comparisons remain unsupported; records/classes require an explicit
operator overload for comparisons.
-**Strings are malloc'd and never freed — by design.** Every string-producing operation
+**Built-in strings are malloc'd and never freed — by design.** Every string-producing operation
(`&`, MID/LEFT/RIGHT/LCASE/UCASE, NUM_TO_STR, CHR, READFILE, CHAR/number→STRING
coercion) mallocs a fresh buffer; string literals and BOOLEAN→STRING coercion instead
return pointers to shared module globals, so a STRING value may alias a global. `MemoryHandler` declares `free` for explicit user memory only; it must never free internal STRING buffers. Don't "fix" leaks piecemeal; it's a global design decision.
diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md
index 427fc45..08301a3 100644
--- a/AGENTS/SUBSYSTEMS.md
+++ b/AGENTS/SUBSYSTEMS.md
@@ -126,7 +126,7 @@ class parameters; `symbolAddress` loads their current object address for all val
lvalue, address-of, receiver and BYREF paths. Field aliases, this and BYREF parameters
bind addresses directly. Assignment-only `NewExprAST` uses checked MemoryHandler
allocation (at least one byte for method-only classes), zero initialization and
-construction, then rebinds reference slots or copies into other destinations. Ordinary assignment always copies. Heap objects require explicit keyword DELETE; INHERITS/SUPER remain unsupported.
+construction, then rebinds reference slots or copies into other destinations. Ordinary assignment invokes an exact member OPERATOR<- when available, otherwise it copies. Heap objects require explicit keyword DELETE; INHERITS/SUPER remain unsupported.
`ConstructorGen.cc` also owns `typeNeedsDestruction` and recursive `emitDestroy`:
ordinary DELETE method first, then fields in reverse declaration order. Explicit
@@ -196,3 +196,5 @@ Builder (RuntimeCheck emits through the same one), saving/restoring the insert b
**Explicit memory** (`MemoryHandler`): lazily emits internal allocation/find/release/resize helpers and a linked list of live allocation start addresses. Explicit MALLOC/REALLOC and keyword NEW allocations enter the list; STRING/ARRAY internals retain their existing ownership. `emitValidate` always calls the find helper, including for null, so keyword DELETE validates before running destructors. FREE validates non-null addresses, unlinks and frees the tracking node, releases the block and returns the original address. Zero-byte allocation returns null. REALLOC validates non-null addresses before its zero-size release path; null behaves as MALLOC. Positive-size resizing updates the node only after success, leaving the old block and record intact on failure. Node allocation precedes the user allocation; if the latter fails, its node is freed before the diagnostic and anonymous throw. Helpers use RuntimeCheck and ExceptionHandler, including source-line arguments and per-function unwind preparation. Sizes accept INTEGER and C integers and convert to native size_t. Address reuse can make stale aliases indistinguishable from current allocations; dereferences are not checked against this list.
**Class copies and temporaries.** Callers construct BYVAL class arguments with a same-class BYREF OVERLOAD NEW; the callee's RegisterCopy hook takes ownership. emitOwnedValue constructs lvalue copies and keeps pending arguments Alive until marshalling succeeds. RETURN copies lvalues and transfers rvalues. StackObject::Temporary uses statement cleanup and deferred temporary-only cleanup before local handlers, while ordinary objects remain function-scoped. Function-result method receivers are materialized by the same path as operator receivers.
+
+**Experimental library** (`pseudocode-lib/`): the default build compiles each .cc with the host compiler into build/pseudocode-lib/*.o and copies .cps units beside them. `_EXP_STRING` is a #LIB class with a single public internal void* handle owning std::string. The `_exp_string_impl_*` C ABI uses checked operations that print __cps_user_line and throw 0 on std::exception. Class copy/assignment methods prevent handle sharing; functions accept BYVAL copies. OUTPUT uses C stdout buffers. Install/release do not yet ship these artifacts.
diff --git a/AGENTS/TESTING.md b/AGENTS/TESTING.md
index 2d7685a..af66e93 100644
--- a/AGENTS/TESTING.md
+++ b/AGENTS/TESTING.md
@@ -23,3 +23,5 @@ All commands run without a shell and retain their exact argv, working directory,
Perl generators use Random.pm's SHA-256 sequence with explicit big-endian extraction, fixed group identity and decimal counter. `generator-vectors.json` pins 100 records plus boundaries using the all-zero 40-character seed; update a vector only when deliberately changing a generator, and verify repeatability and a different seed. C++ references must avoid undefined behavior and implement the specification independently. Use arithmetic closed forms or standard containers where they provide an independent oracle. Record platform-dependent C behavior as such; never call it CIE behavior.
Before committing a test change, build the affected references and run the affected CTest cases. Compiler defects may fail: inspect retained diagnostics and document the failure accurately. Before committing workflow changes, run actionlint and retain the existing release API simulation/package smoke checks without contacting the real release. No compiler implementation changes belong in a tests-only task.
+
+The `_EXP_STRING` integration fixture lives in `tests/library/exp-string`. CMake copies it to the build tree as `test-cases/i/exp-string`, where relative source paths reach the built library and its object file. It uses the ordinary native/runner harness and appears as `i/exp-string` in CTest.
diff --git a/doc/language/keyword/cpsc-keyword-class.7.md b/doc/language/keyword/cpsc-keyword-class.7.md
index feddc07..a46ecda 100644
--- a/doc/language/keyword/cpsc-keyword-class.7.md
+++ b/doc/language/keyword/cpsc-keyword-class.7.md
@@ -69,4 +69,4 @@ OUTPUT Item.Get()
[cpsc-keyword-overload(7)](cpsc-keyword-overload.7.md), [cpsc-language-overview(7)](../cpsc-language-overview.7.md), [cpsc-keyword-delete(7)](cpsc-keyword-delete.7.md), [cpsc-keyword-type(7)](cpsc-keyword-type.7.md), [cpsc-keyword-function(7)](cpsc-keyword-function.7.md), [cpsc-keyword-procedure(7)](cpsc-keyword-procedure.7.md), [cpsc-keyword-call(7)](cpsc-keyword-call.7.md).
-An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
+An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment without an assignment overload. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
diff --git a/doc/language/keyword/cpsc-keyword-delete.7.md b/doc/language/keyword/cpsc-keyword-delete.7.md
index 8979702..d8e2f03 100644
--- a/doc/language/keyword/cpsc-keyword-delete.7.md
+++ b/doc/language/keyword/cpsc-keyword-delete.7.md
@@ -56,4 +56,4 @@ DELETE Object
[cpsc-keyword-class(7)](cpsc-keyword-class.7.md), [cpsc-keyword-declare(7)](cpsc-keyword-declare.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md), [cpsc-function-free(7)](../function/cpsc-function-free.7.md).
-An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
+An `OVERLOAD PROCEDURE NEW(BYREF Other : ClassName)` defines a copy constructor. BYVAL class arguments and RETURN of class lvalues invoke it; a same-class BYVAL constructor is rejected. Classes without one retain bitwise copying. Class results transfer ownership directly into BYVAL arguments, RETURN and ordinary assignment without an assignment overload. Discarded results and temporary receivers live until the statement ends, including exception exits. Ordinary user BYREF calls still require bare variables; constructor calls can bind temporaries.
diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc
index 1f7bc2b..df58f8e 100644
--- a/lib/CodeGen/FunctionGen.cc
+++ b/lib/CodeGen/FunctionGen.cc
@@ -13,7 +13,7 @@ using namespace cps;
// FUNCTION/PROCEDURE with one of these names would hijack the runtime.
static bool isReservedRuntimeName(const std::string &Name) {
static const char *Reserved[] = {
- "__cps_main", "printf", "scanf", "malloc", "free", "strlen", "memcpy",
+ "__cps_main", "__cps_user_line", "printf", "scanf", "malloc", "free", "strlen", "memcpy",
"strcpy", "strcat", "toupper", "tolower", "sprintf", "snprintf", "strtol", "strtoll",
"strtod", "memset", "exit", "fprintf",
"fopen", "fclose", "fgetc", "ungetc", "fputs", "fputc",
diff --git a/pseudocode-lib/README.md b/pseudocode-lib/README.md
new file mode 100644
index 0000000..92a4bbb
--- /dev/null
+++ b/pseudocode-lib/README.md
@@ -0,0 +1,20 @@
+# Experimental string library
+
+Build with `cmake --build build`. Include the built library in your program and pass it as a compiler input:
+
+```text
+INCLUDE "build/pseudocode-lib/_exp_string.cps"
+DECLARE Greeting : _EXP_STRING((C_STRING)"hello")
+CALL _EXP_STRING_OUTPUT(Greeting & _EXP_STRING_NUM_TO_STR(42))
+```
+
+```sh
+build/cpsc prog.cps build/pseudocode-lib/_exp_string.cps
+build/cpsc prog.cps build/pseudocode-lib/_exp_string.cps -r
+```
+
+INCLUDE paths are relative to the program. Adjust them when the program is elsewhere.
+
+`_EXP_STRING` owns an unbounded `std::string` through one handle. Constructors accept no arguments, another `_EXP_STRING`, `C_STRING`, or `(INTEGER count, C_CHAR character)`. Cast literals to C types explicitly; built-in STRING is not a constructor argument. Copy construction and assignment own independent strings. `_HANDLE` is public only for library implementation access; do not modify or free it.
+
+The first version provides `&`, `_EXP_STRING_NUM_TO_STR` (INTEGER/REAL), `_EXP_STRING_STR_TO_NUM` (REAL), `_EXP_STRING_LENGTH` (byte length) and `_EXP_STRING_OUTPUT` (text plus newline). Numeric formatting uses `std::to_string`; parsing uses `std::stod` and requires full consumption. C++ standard exceptions print the user call line and are catchable with TRY/CATCH. The built-in STRING remains available with its existing lifetime and NUL limitations. Owning class values inside whole-record or BYVAL-array copies still have bitwise-copy limitations.
diff --git a/pseudocode-lib/_exp_string.cc b/pseudocode-lib/_exp_string.cc
new file mode 100644
index 0000000..248b718
--- /dev/null
+++ b/pseudocode-lib/_exp_string.cc
@@ -0,0 +1,75 @@
+#include <cstdio>
+#include <exception>
+#include <stdexcept>
+#include <string>
+#include <utility>
+
+extern "C" int __cps_user_line;
+
+namespace {
+template <class Operation>
+decltype(auto) checked(Operation &&Op) {
+ try {
+ return std::forward<Operation>(Op)();
+ } catch (const std::exception &Error) {
+ std::fprintf(stderr, "[Fatal] line %d: %s\n", __cps_user_line, Error.what());
+ throw 0;
+ }
+}
+std::string &text(void *Handle) { return *static_cast<std::string *>(Handle); }
+}
+
+extern "C" {
+void *_exp_string_impl_new() {
+ return checked([] { return new std::string; });
+}
+void *_exp_string_impl_copy(void *Other) {
+ return checked([&] { return new std::string(text(Other)); });
+}
+void *_exp_string_impl_from_cstr(const char *Text) {
+ return checked([&] { return new std::string(Text); });
+}
+void *_exp_string_impl_from_count_char(long long Count, char Ch) {
+ return checked([&] {
+ if (Count < 0) throw std::invalid_argument("negative string length");
+ return new std::string(static_cast<std::size_t>(Count), Ch);
+ });
+}
+void _exp_string_impl_delete(void *Handle) {
+ checked([&] { delete static_cast<std::string *>(Handle); });
+}
+void _exp_string_impl_assign(void *Target, void *Other) {
+ checked([&] { text(Target) = text(Other); });
+}
+void _exp_string_impl_assign_cstr(void *Target, const char *Text) {
+ checked([&] { text(Target) = Text; });
+}
+void _exp_string_impl_concat(void *Target, void *A, void *B) {
+ checked([&] { text(Target) = text(A) + text(B); });
+}
+void _exp_string_impl_from_int(void *Target, long long Number) {
+ checked([&] { text(Target) = std::to_string(Number); });
+}
+void _exp_string_impl_from_real(void *Target, double Number) {
+ checked([&] { text(Target) = std::to_string(Number); });
+}
+double _exp_string_impl_to_real(void *Handle) {
+ return checked([&] {
+ std::size_t Consumed = 0;
+ double Number = std::stod(text(Handle), &Consumed);
+ if (Consumed != text(Handle).size())
+ throw std::invalid_argument("unconsumed characters in string conversion");
+ return Number;
+ });
+}
+long long _exp_string_impl_length(void *Handle) {
+ return checked([&] { return static_cast<long long>(text(Handle).size()); });
+}
+void _exp_string_impl_output(void *Handle) {
+ checked([&] {
+ const auto &Text = text(Handle);
+ std::fwrite(Text.data(), 1, Text.size(), stdout);
+ std::fputc('\n', stdout);
+ });
+}
+}
diff --git a/pseudocode-lib/_exp_string.cps b/pseudocode-lib/_exp_string.cps
new file mode 100644
index 0000000..369e20f
--- /dev/null
+++ b/pseudocode-lib/_exp_string.cps
@@ -0,0 +1,67 @@
+#LIB
+INCLUDE_OBJ "_exp_string.o"
+TYPE _EXP_STRING_HANDLE = C_TYPE("void*")
+TYPE _EXP_STRING_C_INTEGER = C_TYPE("long long")
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_new() RETURNS _EXP_STRING_HANDLE
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_copy(_EXP_STRING_HANDLE) RETURNS _EXP_STRING_HANDLE
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_from_cstr(C_STRING) RETURNS _EXP_STRING_HANDLE
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_from_count_char(_EXP_STRING_C_INTEGER, C_CHAR) RETURNS _EXP_STRING_HANDLE
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_delete(_EXP_STRING_HANDLE)
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_assign(_EXP_STRING_HANDLE, _EXP_STRING_HANDLE)
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_assign_cstr(_EXP_STRING_HANDLE, C_STRING)
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_concat(_EXP_STRING_HANDLE, _EXP_STRING_HANDLE, _EXP_STRING_HANDLE)
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_from_int(_EXP_STRING_HANDLE, _EXP_STRING_C_INTEGER)
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_from_real(_EXP_STRING_HANDLE, C_DOUBLE)
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_to_real(_EXP_STRING_HANDLE) RETURNS C_DOUBLE
+EXTERN "C" EXTERN_FUNCTION _exp_string_impl_length(_EXP_STRING_HANDLE) RETURNS _EXP_STRING_C_INTEGER
+EXTERN "C" EXTERN_PROCEDURE _exp_string_impl_output(_EXP_STRING_HANDLE)
+
+CLASS _EXP_STRING
+ PUBLIC _HANDLE : _EXP_STRING_HANDLE
+ OVERLOAD PROCEDURE NEW()
+ _HANDLE <- _exp_string_impl_new()
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE NEW(BYREF Other : _EXP_STRING)
+ _HANDLE <- _exp_string_impl_copy(Other._HANDLE)
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE NEW(Text : C_STRING)
+ _HANDLE <- _exp_string_impl_from_cstr(Text)
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE NEW(Count : INTEGER, Ch : C_CHAR)
+ _HANDLE <- _exp_string_impl_from_count_char((_EXP_STRING_C_INTEGER)Count, Ch)
+ ENDPROCEDURE
+ PROCEDURE DELETE()
+ CALL _exp_string_impl_delete(_HANDLE)
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE OPERATOR<-(BYREF Other : _EXP_STRING)
+ CALL _exp_string_impl_assign(_HANDLE, Other._HANDLE)
+ ENDPROCEDURE
+ OVERLOAD PROCEDURE OPERATOR<-(Text : C_STRING)
+ CALL _exp_string_impl_assign_cstr(_HANDLE, Text)
+ ENDPROCEDURE
+ENDCLASS
+
+OVERLOAD FUNCTION OPERATOR&(A : _EXP_STRING, B : _EXP_STRING) RETURNS _EXP_STRING
+ DECLARE Result : _EXP_STRING
+ CALL _exp_string_impl_concat(Result._HANDLE, A._HANDLE, B._HANDLE)
+ RETURN Result
+ENDFUNCTION
+OVERLOAD FUNCTION _EXP_STRING_NUM_TO_STR(Number : INTEGER) RETURNS _EXP_STRING
+ DECLARE Result : _EXP_STRING
+ CALL _exp_string_impl_from_int(Result._HANDLE, (_EXP_STRING_C_INTEGER)Number)
+ RETURN Result
+ENDFUNCTION
+OVERLOAD FUNCTION _EXP_STRING_NUM_TO_STR(Number : REAL) RETURNS _EXP_STRING
+ DECLARE Result : _EXP_STRING
+ CALL _exp_string_impl_from_real(Result._HANDLE, (C_DOUBLE)Number)
+ RETURN Result
+ENDFUNCTION
+FUNCTION _EXP_STRING_STR_TO_NUM(S : _EXP_STRING) RETURNS REAL
+ RETURN (REAL)_exp_string_impl_to_real(S._HANDLE)
+ENDFUNCTION
+FUNCTION _EXP_STRING_LENGTH(S : _EXP_STRING) RETURNS INTEGER
+ RETURN (INTEGER)_exp_string_impl_length(S._HANDLE)
+ENDFUNCTION
+PROCEDURE _EXP_STRING_OUTPUT(S : _EXP_STRING)
+ CALL _exp_string_impl_output(S._HANDLE)
+ENDPROCEDURE
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1f13dea..c5b33a1 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -30,3 +30,17 @@ foreach(suite i ii iii)
LABELS "${suite}" TIMEOUT 900)
endforeach()
endforeach()
+
+# Keep the library beside its built objects without assuming a build directory name.
+set(cpsc_library_case "${CMAKE_BINARY_DIR}/test-cases/i/exp-string")
+foreach(file program.cps case.json expected.txt expected-stderr.txt)
+ configure_file("library/exp-string/${file}" "${cpsc_library_case}/${file}" COPYONLY)
+endforeach()
+add_test(NAME "i/exp-string" COMMAND "${PERL_EXECUTABLE}"
+ "${CMAKE_CURRENT_SOURCE_DIR}/run.pl"
+ --case "${cpsc_library_case}/case.json" --cpsc "$<TARGET_FILE:cpsc>"
+ --run-mode "${CPSC_RUN_MODE}" --perl "${PERL_EXECUTABLE}"
+ --cc "${CMAKE_C_COMPILER}" --cxx "${CMAKE_CXX_COMPILER}"
+ --seed "${CPSC_TEST_SEED}" --count "${CPSC_TEST_COUNT}"
+ --results "${CMAKE_BINARY_DIR}/test-results")
+set_tests_properties("i/exp-string" PROPERTIES LABELS "i" TIMEOUT 900)
diff --git a/tests/README.md b/tests/README.md
index 3b9cfc7..ab05c48 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -11,7 +11,7 @@ perl tests/summarize.pl build/test-results
Use `ctest --test-dir build -L '^ii$' --output-on-failure` for one suite, or `-R '^i/arrays$'` for one case. CTest returns failure if any case fails; it continues through the other cases. There are no expected-failure exemptions. Rejection of valid CIE syntax must not be treated as a successful negative test.
-The suites are **i**: independently traced fixed outputs, **ii**: C++ differential tests with Perl inputs, and **iii**: rejected programs with required causes and source lines. Fixed expectations are **pending human review**; each case's notes explain the trace and distinguish CIE rules from project extensions. REAL calculations are observed through BOOLEAN/INTEGER output; REAL text and REAL-to-string formatting remain outside this suite.
+The suites are **i**: independently traced fixed outputs, **ii**: C++ differential tests with Perl inputs, and **iii**: rejected programs with required causes and source lines. Fixed expectations are **pending human review**; each case's notes explain the trace and distinguish CIE rules from project extensions. REAL calculations are observed through BOOLEAN/INTEGER output; The experimental string-library fixture additionally checks its specified C++17 numeric text formatting.
The default is 10,000 random records per differential group, plus fixed boundary records. Both generated native programs and the configured `-r` path are checked; NONE builds only run native programs. Each source is compiled once per execution path and processes its records in one batch. Tests normalize only CRLF to LF; spaces, blank lines, case and final newlines remain significant. Generated text files use the same comparison rule.
diff --git a/tests/i/class-copy/notes.md b/tests/i/class-copy/notes.md
new file mode 100644
index 0000000..eab1cbf
--- /dev/null
+++ b/tests/i/class-copy/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+Each completed default/copy construction increments Live; only copy construction increments Copies. Pass destroys its independent modified copy. Make copies its local into the result before destroying the local. Forward transfers that result. Discarded results, temporary receivers and constructor-bound results are destroyed at statement end. A later argument throwing destroys the already-created argument before entering CATCH. At the final count, nine copies have completed and only A, B and C remain live; reverse destruction removes values 8, 1 and 1. Remaining is zero. This is the class lifetime extension.
diff --git a/tests/i/escape-sequences/notes.md b/tests/i/escape-sequences/notes.md
new file mode 100644
index 0000000..432300e
--- /dev/null
+++ b/tests/i/escape-sequences/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+Simple escapes produce a literal quote, backslash and question mark. Octal 101 and hexadecimal 42 add A and B after the leading A; Unicode escapes encode the displayed accented letter and emoji. Escaped CHAR byte values are 10, 9 and 255. Built-in STRING stops at the embedded NUL.
diff --git a/tests/i/extern-cc-runtime/notes.md b/tests/i/extern-cc-runtime/notes.md
new file mode 100644
index 0000000..7ea9429
--- /dev/null
+++ b/tests/i/extern-cc-runtime/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+A C++ string starts with 4096 x bytes and appends the three digits 123, giving 4099. The second EXTERN function catches and rethrows invalid_argument; pseudocode catches it once. These are C++ object and exception extensions.
diff --git a/tests/i/library-lines/notes.md b/tests/i/library-lines/notes.md
new file mode 100644
index 0000000..b18e19c
--- /dev/null
+++ b/tests/i/library-lines/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+User calls on lines 3, 8 and 13 enter nested library array access, an array-argument contract and a close of an unopened file. The declaration on line 18 automatically invokes a library constructor that divides by zero. Each error uses that user line and is caught. These are library and exception extensions.
diff --git a/tests/i/operator-assign/notes.md b/tests/i/operator-assign/notes.md
new file mode 100644
index 0000000..da63a77
--- /dev/null
+++ b/tests/i/operator-assign/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+Assignments copy 3 into B, self-assign 3, then copy the returned 7. C_STRING assignment sets A to length 5 without a copy trace. Record-field, array-element and dereferenced assignments trace 7, 5 and 7. Final fields are 7, 7, 7 and 5. This is the member assignment extension.
diff --git a/tests/i/underscore-identifiers/notes.md b/tests/i/underscore-identifiers/notes.md
new file mode 100644
index 0000000..1941f03
--- /dev/null
+++ b/tests/i/underscore-identifiers/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+The underscore-prefixed class holds an INTEGER. The underscore-prefixed function adds one to 41 and the underscore-prefixed variable receives 42. This is a compiler identifier extension.
diff --git a/tests/library/exp-string/case.json b/tests/library/exp-string/case.json
new file mode 100644
index 0000000..a558ad6
--- /dev/null
+++ b/tests/library/exp-string/case.json
@@ -0,0 +1 @@
+{"sources":["program.cps","../../../pseudocode-lib/_exp_string.cps"]}
diff --git a/tests/library/exp-string/expected-stderr.txt b/tests/library/exp-string/expected-stderr.txt
new file mode 100644
index 0000000..6dc689f
--- /dev/null
+++ b/tests/library/exp-string/expected-stderr.txt
@@ -0,0 +1,2 @@
+[Fatal] line 27: unconsumed characters in string conversion
+[Fatal] line 32: negative string length
diff --git a/tests/library/exp-string/expected.txt b/tests/library/exp-string/expected.txt
new file mode 100644
index 0000000..fab16b1
--- /dev/null
+++ b/tests/library/exp-string/expected.txt
@@ -0,0 +1,10 @@
+helloworld
+0
+3
+20500
+20500
+421.250000
+TRUE
+conversion caught
+length caught
+heap
diff --git a/tests/library/exp-string/notes.md b/tests/library/exp-string/notes.md
new file mode 100644
index 0000000..ca7d489
--- /dev/null
+++ b/tests/library/exp-string/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+Copies of hello are independent; assigning world to the copy gives helloworld when concatenated. Empty length is zero, and three NUL bytes retain length three. One hundred appended five-byte words grow 20000 bytes to 20500, preserved by BYVAL and RETURN. C++17 formatting gives 42 and 1.250000. Parsing 12.5 compares equal to 12.5. Trailing junk and negative counts emit the explicit library reasons at user lines 27 and 32, then are caught. Discarded numeric and Echo results must release their owned strings at statement end. The explicitly deleted heap object prints heap. This fixture covers compiler/library extensions, including numeric text formatting.
diff --git a/tests/library/exp-string/program.cps b/tests/library/exp-string/program.cps
new file mode 100644
index 0000000..719d9a4
--- /dev/null
+++ b/tests/library/exp-string/program.cps
@@ -0,0 +1,41 @@
+INCLUDE "../../../pseudocode-lib/_exp_string.cps"
+FUNCTION Echo(S : _EXP_STRING) RETURNS _EXP_STRING
+ RETURN S
+ENDFUNCTION
+DECLARE A : _EXP_STRING((C_STRING)"hello")
+DECLARE B : _EXP_STRING(A)
+B <- (C_STRING)"world"
+B <- B
+CALL _EXP_STRING_OUTPUT(A & B)
+DECLARE Empty : _EXP_STRING
+OUTPUT _EXP_STRING_LENGTH(Empty)
+DECLARE Nuls : _EXP_STRING(3, (C_CHAR)'\0')
+OUTPUT _EXP_STRING_LENGTH(Nuls)
+DECLARE Long : _EXP_STRING(20000, (C_CHAR)'x')
+DECLARE I : INTEGER
+FOR I <- 1 TO 100
+ Long <- Long & A
+NEXT I
+OUTPUT _EXP_STRING_LENGTH(Long)
+Long <- Echo(Long)
+OUTPUT _EXP_STRING_LENGTH(Long)
+CALL _EXP_STRING_OUTPUT(_EXP_STRING_NUM_TO_STR(42) & _EXP_STRING_NUM_TO_STR(1.25))
+DECLARE Number : _EXP_STRING((C_STRING)"12.5")
+OUTPUT _EXP_STRING_STR_TO_NUM(Number) = 12.5
+TRY
+ Number <- (C_STRING)"12x"
+ OUTPUT _EXP_STRING_STR_TO_NUM(Number)
+CATCH
+ OUTPUT "conversion caught"
+ENDTRY
+TRY
+ DECLARE Invalid : _EXP_STRING(-1, (C_CHAR)'x')
+CATCH
+ OUTPUT "length caught"
+ENDTRY
+CALL _EXP_STRING_NUM_TO_STR(99)
+CALL Echo(Long)
+DECLARE Heap : _EXP_STRING
+Heap <- NEW _EXP_STRING((C_STRING)"heap")
+CALL _EXP_STRING_OUTPUT(Heap)
+DELETE Heap