aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-27 14:07:24 +0000
committer_ov4 <ov4@ov4.org>2026-10-27 14:07:24 +0000
commitcaa88a9b36d9e1dfae40ecb31aee05bc2891fa17 (patch)
treed41168dc0799c6f8b033a3d6fdb2c2c72aac2b42
parent5d37ebcc02f6ca2ce1ced93f803dd1daf634d866 (diff)
docs(preprocessor): document switches and library regions
Add manuals and aliases for all five PP directives, replace the retired #LIB page, and update compiler and shared-editor architecture notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS/ARCH.md4
-rw-r--r--AGENTS/ARCH/DRIVER.md3
-rw-r--r--AGENTS/ARCH/LAYOUT.md1
-rw-r--r--AGENTS/ARCH/OVERVIEW.md10
-rw-r--r--AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md14
-rw-r--r--AGENTS/CONVENTIONS/LEXING.md13
-rw-r--r--AGENTS/EXTENDING.md2
-rw-r--r--AGENTS/SUBSYSTEMS/CLASS_LIFETIME.md4
-rw-r--r--AGENTS/SUBSYSTEMS/MEMORY.md4
-rw-r--r--AGENTS/SUBSYSTEMS/STDLIB.md6
-rw-r--r--AGENTS/SUBSYSTEMS/TEMPLATES.md2
-rw-r--r--bundle/AGENTS/DEVELOPMENT.md2
-rw-r--r--cmake/ManualAliases.cmake3
-rw-r--r--doc/cpsc.1.md2
-rw-r--r--doc/language/cpsc-language-overview.7.md9
-rw-r--r--doc/language/keyword/cpsc-keyword-lib.7.md31
-rw-r--r--doc/language/keyword/cpsc-keyword-pp_define.7.md45
-rw-r--r--doc/language/keyword/cpsc-keyword-pp_ifdef.7.md45
-rw-r--r--plugin/AGENTS/ARCH.md11
19 files changed, 153 insertions, 58 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md
index 1474fac..4926b52 100644
--- a/AGENTS/ARCH.md
+++ b/AGENTS/ARCH.md
@@ -5,7 +5,7 @@ This file is an index; the detail lives in `AGENTS/ARCH/`.
| File | Contents |
|---|---|
-| [OVERVIEW.md](ARCH/OVERVIEW.md) | Three-stage pipeline, fixed value representation (ABI), C interop types, pointer to language manuals |
+| [OVERVIEW.md](ARCH/OVERVIEW.md) | Preprocessing and compiler pipeline, fixed value representation (ABI), C interop types, pointer to language manuals |
| [BUILD.md](ARCH/BUILD.md) | Build and run commands, run modes, exception ABI, runtime errors, CI overview |
| [DEPENDENCIES.md](ARCH/DEPENDENCIES.md) | Local/system dependency lookup, compiler selection, MSYS2 notes, LLVM 23 pin |
| [DRIVER.md](ARCH/DRIVER.md) | `tools/driver/main.cc`: options, multi-file inputs, entry points, output and exit-code rules, PATH/NONE modes |
@@ -15,7 +15,7 @@ This file is an index; the detail lives in `AGENTS/ARCH/`.
| [NIGHTLY.md](ARCH/NIGHTLY.md) | `nightly-build.yml` jobs, Bundle packages, macOS LLD/Rust caching, publish rules, package naming |
| [LAYOUT.md](ARCH/LAYOUT.md) | Source file map (lexer, parser, codegen, CMake, installers), AST conventions |
-`#LIB` library units are described in [CONVENTIONS/EXCEPTIONS_AND_UNITS.md](CONVENTIONS/EXCEPTIONS_AND_UNITS.md);
+`PP_DEFINE __LIB` library regions are described in [CONVENTIONS/EXCEPTIONS_AND_UNITS.md](CONVENTIONS/EXCEPTIONS_AND_UNITS.md);
the embedded standard library in [SUBSYSTEMS/STDLIB.md](SUBSYSTEMS/STDLIB.md).
## Detail docs
diff --git a/AGENTS/ARCH/DRIVER.md b/AGENTS/ARCH/DRIVER.md
index ef739b2..974c0dc 100644
--- a/AGENTS/ARCH/DRIVER.md
+++ b/AGENTS/ARCH/DRIVER.md
@@ -6,7 +6,8 @@ style is in [CONVENTIONS/SYMBOLS_AND_ERRORS.md](../CONVENTIONS/SYMBOLS_AND_ERROR
- One or more source files, with optional `-S`, `-o path`, `-r`, `-no-std` and
`-fdiagnostics-color[=auto|always|never]` / `-fno-diagnostics-color`; options may appear among inputs.
- Each stream is parsed independently. INCLUDE paths are relative to the including file,
+ Each input is preprocessed independently before parsing. PP_* switches are file-local;
+ masked source retains line numbers, and SourceUnit owns the per-line __LIB map. INCLUDE paths are relative to the including file,
matched against command-line inputs with fs::equivalent. The driver rejects repeated
inputs and computes transitive symbol visibility; cycles do not copy statements.
CodeGen records owners for signatures, types and exported constants/globals and filters
diff --git a/AGENTS/ARCH/LAYOUT.md b/AGENTS/ARCH/LAYOUT.md
index c2cfee2..44a8f51 100644
--- a/AGENTS/ARCH/LAYOUT.md
+++ b/AGENTS/ARCH/LAYOUT.md
@@ -3,6 +3,7 @@
| Path | Contents |
|---|---|
| `include/cps/` | All headers, included as `"cps/Foo.h"` |
+| `lib/Lexer/Preprocessor.cc`, `include/cps/Preprocessor.h` | File-local switch preprocessing, conditional masking and original-line library metadata |
| `lib/Lexer/Lexer.cc` | Token scanning and payload-preserving lookahead; `gettok()` consumes and `peekToken`/`peekLine` inspect buffered tokens |
| `lib/Parser/Parser.cc` | Entry point, statement dispatch, assignment parsing and error recovery |
| `lib/Parser/ParserExpression.cc`, `ParserControlFlow.cc` | Expressions/designators and control-flow statement parsing (including ordered CASE branches) |
diff --git a/AGENTS/ARCH/OVERVIEW.md b/AGENTS/ARCH/OVERVIEW.md
index 820c5bb..ee3fc12 100644
--- a/AGENTS/ARCH/OVERVIEW.md
+++ b/AGENTS/ARCH/OVERVIEW.md
@@ -1,14 +1,14 @@
# Overview and value representation
-`cpsc` compiles CIE A-Level 9618 pseudocode to native executables or LLVM IR. Classic three-stage
-Kaleidoscope-style design, C++17, built with LLVM 23 and Clang 23. CMake rejects
+`cpsc` compiles CIE A-Level 9618 pseudocode to native executables or LLVM IR. A line preprocessor feeds a
+Kaleidoscope-style lexer/parser/codegen pipeline, C++17, built with LLVM 23 and Clang 23. CMake rejects
other major versions and non-Clang compilers. Execution is selected at
build time: external lli, an embedded static LLVM engine, or disabled.
```
-files ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> CodeGen ──> LLVM IR ──> NativeCompiler (object emission + embedded LLD)
- (istream) (recursive (flat program list; (one llvm::Module "cps_module",
- descent) FunctionDefAST mixed in) entry code inside i32 main())
+files -> Preprocessor -> Lexer -> Parser -> AST statements -> CodeGen -> LLVM IR -> NativeCompiler
+ (per-file (istream) (recursive descent) (one module) (object + embedded LLD)
+ switches and source-line library metadata)
```
Fixed value representation (ABI — much code infers types backwards from these):
diff --git a/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md b/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md
index 8ee97ec..0b8a887 100644
--- a/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md
+++ b/AGENTS/CONVENTIONS/EXCEPTIONS_AND_UNITS.md
@@ -35,10 +35,16 @@ Part of [Load-bearing conventions](../CONVENTIONS.md).
through ExceptionHandler, truncates its result and retains the final uncaught
handler.
-**Library units.** Line-leading `#LIB` marks SourceUnit::IsLibrary. RuntimeCheck uses
-the externally visible i32 `__cps_user_line`; FunctionGen's shared call hook updates
-it only on non-library to library calls. Runtime helper line arguments use
-RuntimeCheck::lineValue too. Compiler diagnostics retain the owning file location.
+**Library regions.** PP_DEFINE __LIB enables library mode for subsequent source
+positions; PP_UNDEF __LIB disables it, including inside functions. SourceUnit owns
+LibraryLines and answers isLibrary(Line). RuntimeCheck queries the current unit's
+map with each original source line and uses the shared external i32 __cps_user_line
+only in enabled regions. FunctionGen's call hook updates that global for calls
+from ordinary positions, regardless of the callee's header mode: a callee can
+enable __LIB inside its body. Library calls preserve it. Runtime helper arguments
+also use RuntimeCheck::lineValue. Templates keep definition source lines and
+owning units; deferred object cleanup keeps its registration's line and unit.
+Compiler diagnostics retain the owning file location.
**Default standard library.** The driver extracts embedded std.cps, its imported units
and matching objects to a content-keyed cache (or temporary fallback) that survives
diff --git a/AGENTS/CONVENTIONS/LEXING.md b/AGENTS/CONVENTIONS/LEXING.md
index 81b01d1..654d7ae 100644
--- a/AGENTS/CONVENTIONS/LEXING.md
+++ b/AGENTS/CONVENTIONS/LEXING.md
@@ -20,7 +20,8 @@ identifier. Assignment is `<-` (ASCII only), equality is single `=`, inequality
concatenation `&` (reaches the parser as raw ASCII 38 — no token). Identifiers use
ASCII case-insensitive lookup/comparison through `IdentifierMap`, `IdentifierSet`
and `sameIdentifier`; retain source spelling in ASTs and diagnostics. This includes
-functions, types, enum values, fields and builtins, but not C_TYPE import strings.
+functions, types, enum values, fields and builtins, but not C_TYPE import strings
+or preprocessor switch names.
Integer tokens carry a checked unsigned magnitude through lookahead; the parser
applies the sign and i64 range check, including the minimum value. String/char
literals support C++17 byte escapes and Unicode escapes (UTF-8 in STRING, ASCII only
@@ -32,3 +33,13 @@ reserved in every user identifier, including EXTERN declarations.
**The lexer reads a caller-owned stream.** Construct it with `Lexer(std::istream &)`;
the stream must outlive the lexer. Lookahead, line number and error state belong to
each instance. The driver opens the source file; stdin is not used for compilation.
+
+**Preprocessing precedes lexing.** `preprocess(std::istream &)` returns masked source,
+per-line __LIB states and an error flag. PP_DEFINE/PP_UNDEF change case-sensitive,
+file-local switches; PP_IFDEF/PP_IFNDEF/PP_ENDIF maintain a nested conditional stack.
+Directives occupy a line, with optional leading whitespace and trailing // comments.
+Disabled ordinary source is never tokenized; DEFINE/UNDEF in it have no effect.
+Masking preserves byte lengths, CRLF and original source line numbers. The driver
+owns the processed stream passed to Lexer. PP_* keywords remain reserved outside
+valid directives. There is no macro expansion, value, PP_ELSE or switch propagation
+through INCLUDE; line-leading # directives are errors.
diff --git a/AGENTS/EXTENDING.md b/AGENTS/EXTENDING.md
index 358e80e..dc3be08 100644
--- a/AGENTS/EXTENDING.md
+++ b/AGENTS/EXTENDING.md
@@ -3,7 +3,7 @@
Step-by-step patterns for adding features. Read [CONVENTIONS.md](CONVENTIONS.md) first — every recipe
below assumes those invariants.
-**New statement:** token in `Lexer.h` enum + keyword match in `Lexer.cc`'s if-chain →
+**New statement:** token in `Lexer.h` enum + entry in `Lexer.cc`'s Keywords table →
node class in `AST.h` or a feature header (`FunctionAST.h`/`FileAST.h` precedent;
unique_ptr children, move ctor, raw-ptr getters) → `ParseXxx` in
the matching `Parser*.cc` file + branch in `ParseStatementImpl` (block statements parse their body with
diff --git a/AGENTS/SUBSYSTEMS/CLASS_LIFETIME.md b/AGENTS/SUBSYSTEMS/CLASS_LIFETIME.md
index 98693e5..19be0aa 100644
--- a/AGENTS/SUBSYSTEMS/CLASS_LIFETIME.md
+++ b/AGENTS/SUBSYSTEMS/CLASS_LIFETIME.md
@@ -37,7 +37,9 @@ Part of the [subsystem notes](../SUBSYSTEMS.md). Builds on [classes](CLASSES.md)
- Stack lifetimes use a per-function `StackObjects` list saved/restored around function
emission. DECLARE and BYVAL copies share ownership registration, including arrays and
records with class fields.
-- Each record retains the original address, an entry-initialized Alive flag and, for
+- Each record retains its original source line and owning unit so deferred cleanup
+ uses the correct __LIB region, even for globals from another unit. It also retains
+ the original address, an entry-initialized Alive flag and, for
arrays, a runtime element count in an entry slot. Array buffer addresses also live in
entry slots so deferred cleanup never references branch-local bound SSA values.
- Every array registers, even when its elements need no destruction: cleanup runs the
diff --git a/AGENTS/SUBSYSTEMS/MEMORY.md b/AGENTS/SUBSYSTEMS/MEMORY.md
index 40cfe9e..ab752de 100644
--- a/AGENTS/SUBSYSTEMS/MEMORY.md
+++ b/AGENTS/SUBSYSTEMS/MEMORY.md
@@ -14,8 +14,8 @@ C ABI lowering, library call-line hook and exception-aware calls.
- memory.cc owns a linked list of live allocation start addresses. Explicit
MALLOC/REALLOC and keyword NEW share it; STRING/ARRAY internals keep their own
ownership outside the list.
-- Errors print the existing reason with __cps_user_line and throw 0. The #LIB call
- hook supplies the user line, including keyword NEW/DELETE helper calls.
+- Errors print the existing reason with __cps_user_line and throw 0. The call hook
+ supplies the user line from ordinary source positions, including keyword NEW/DELETE helper calls.
- Sizes accept INTEGER and every supported C integer (including _Bool) through
ordinary overloads. Wrappers explicitly cast to unsigned long long; C++ converts
that value to native size_t, preserving the old integer conversion behavior.
diff --git a/AGENTS/SUBSYSTEMS/STDLIB.md b/AGENTS/SUBSYSTEMS/STDLIB.md
index 6c7b14c..07d0959 100644
--- a/AGENTS/SUBSYSTEMS/STDLIB.md
+++ b/AGENTS/SUBSYSTEMS/STDLIB.md
@@ -1,6 +1,6 @@
# Standard library
-Part of the [subsystem notes](../SUBSYSTEMS.md). `#LIB` library units and default
+Part of the [subsystem notes](../SUBSYSTEMS.md). `PP_DEFINE __LIB` library regions and default
import rules: [CONVENTIONS/EXCEPTIONS_AND_UNITS.md](../CONVENTIONS/EXCEPTIONS_AND_UNITS.md).
**Standard library** (`pseudocode-lib/`)
@@ -31,8 +31,8 @@ import rules: [CONVENTIONS/EXCEPTIONS_AND_UNITS.md](../CONVENTIONS/EXCEPTIONS_AN
## STRING and I/O bridging
-- STRING is a #LIB class with a public internal void* handle owning std::string. The
- _string_impl_* C ABI reports __cps_user_line and throws 0 on std::exception.
+- The STRING class is defined under PP_DEFINE __LIB. Its public internal void*
+ handle owns std::string. The _string_impl_* C ABI reports __cps_user_line and throws 0 on std::exception.
- Functions/operators use ordinary resolution; StringValueGen bridges literals, C_STR, C
conversions and file operands.
- C_STRING literals have an adjacent uppercase C suffix.
diff --git a/AGENTS/SUBSYSTEMS/TEMPLATES.md b/AGENTS/SUBSYSTEMS/TEMPLATES.md
index 95d2963..b80f7ed 100644
--- a/AGENTS/SUBSYSTEMS/TEMPLATES.md
+++ b/AGENTS/SUBSYSTEMS/TEMPLATES.md
@@ -57,7 +57,7 @@ callable lookup.
- After all units' statement loops, `drainTemplateBodies` emits queued functions and
class methods through `emitStmt`, repeating for newly requested instances until the
queue is empty. Instantiation depth is limited to 64.
-- `selectUnit` restores the template owner's file, library mode and call hook;
+- `selectUnit` restores the template owner's file, library-line lookup and call hook;
`VisibilityUnit` uses the first requester's transitive visibility, including
requester-defined operators needed by a library template.
- Each body receives the symbol snapshot from the template declaration, preserving
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md
index d63bbca..239d510 100644
--- a/bundle/AGENTS/DEVELOPMENT.md
+++ b/bundle/AGENTS/DEVELOPMENT.md
@@ -24,6 +24,8 @@ The editor imports the plugin's grammar, language configuration and pure CommonJ
Windows and macOS nightly jobs cache Rust dependencies from `src-tauri/target` after selecting the Rust toolchain. The cache includes compiled dependencies and is saved even if a later check fails; project crates are rebuilt so the packaged compiler comes from the current checkout. Rust toolchain and Cargo configuration changes affect the cache key. Release optimization and archive compression settings are unchanged. Compare cold and warm CI runs to measure the benefit; a cache miss must still build normally.
+PP_* highlighting comes from the shared plugin grammar. Conditional directives add no indentation; switch evaluation and __LIB region behavior belong to the packaged compiler. The native runner test includes a switch-controlled program.
+
The language editing core (grammar, configuration, indentation, lexical masking and bracket matching) is shared in full. Platform editor API adapters, file dialogs and process management are separate. Keep the plugin free of Bundle runtime dependencies.
The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. Each window label has its own Runner, so windows run independently; a watch signal kills and reaps the process on Stop, when its window is destroyed, or on `RunEvent::Exit`. User-requested application exit (including Dock Quit) closes the windows through their frontend handlers before allowing the app to exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal. The runner passes `-fdiagnostics-color=always`; `src/ansi.js` renders SGR bold and foreground colors from stderr only, so program stdout stays verbatim. Colors use `--vscode-terminal-ansi*`, which fall back to VS Code's terminal dark defaults pinned in `vendor/vscode/terminal-colors.json` because 2026 Dark does not set them.
diff --git a/cmake/ManualAliases.cmake b/cmake/ManualAliases.cmake
index 858e6c9..6f2430a 100644
--- a/cmake/ManualAliases.cmake
+++ b/cmake/ManualAliases.cmake
@@ -3,6 +3,9 @@
# The NAME lines in the primary .md sources are the human-readable
# counterpart and must be kept in sync with this map.
set(CPSC_MANUAL_ALIASES
+ "cpsc-keyword-pp_undef:cpsc-keyword-pp_define"
+ "cpsc-keyword-pp_ifndef:cpsc-keyword-pp_ifdef"
+ "cpsc-keyword-pp_endif:cpsc-keyword-pp_ifdef"
"cpsc-keyword-then:cpsc-keyword-if"
"cpsc-keyword-else:cpsc-keyword-if"
"cpsc-keyword-endif:cpsc-keyword-if"
diff --git a/doc/cpsc.1.md b/doc/cpsc.1.md
index e0e92e0..d5a8d72 100644
--- a/doc/cpsc.1.md
+++ b/doc/cpsc.1.md
@@ -16,6 +16,8 @@ Compile one or more pseudocode source files to a native executable, or to textua
Without **-o**, save beside the entry source file, replacing its extension with `.exe` on Windows or `.out` elsewhere for an executable, or `.ll` for IR. With **-r** alone, run without saving a file.
+Each input is independently preprocessed using file-local PP_DEFINE/PP_UNDEF switches and PP_IFDEF/PP_IFNDEF/PP_ENDIF conditions. PP_DEFINE __LIB enables library runtime line reporting for subsequent source positions; PP_UNDEF __LIB restores ordinary behavior. See [cpsc-keyword-pp_define(7)](language/keyword/cpsc-keyword-pp_define.7.md).
+
Each input is a compilation unit. Use top-level `INCLUDE "lib.cps"` to import another input's exported definitions, including its transitive imports. Paths are relative to the including source; every included file must also appear on the command line. Repeated includes and include cycles are harmless. Giving the same input twice, including symbolic or hard links, is an error. Duplicate definitions across files are errors even if the files do not include each other; distinct signatures, differing in parameters or return type, may share a name when every definition specifies OVERLOAD.
Exactly one file may contain top-level executable statements (including top-level DECLARE). Alternatively, define a parameterless `FUNCTION MAIN RETURNS INTEGER` in one file and have no top-level executable statements anywhere. Builds containing only definitions and no MAIN fail with `error: no main program`. GLOBAL blocks declare shared primitive variables. INCLUDE, GLOBAL and MAIN entry points are compiler extensions beyond the Cambridge guide.
diff --git a/doc/language/cpsc-language-overview.7.md b/doc/language/cpsc-language-overview.7.md
index 2c2019c..559a58d 100644
--- a/doc/language/cpsc-language-overview.7.md
+++ b/doc/language/cpsc-language-overview.7.md
@@ -6,7 +6,7 @@ cpsc-language-overview - supported cpsc pseudocode syntax
# DESCRIPTION
-Keywords are uppercase and case-sensitive. Identifiers, including built-in function names, are case insensitive. Indentation is for readability; blocks use explicit closing keywords. Identifiers begin with a letter or underscore and continue with letters, digits, or underscores.
+Keywords are uppercase and case-sensitive. Ordinary identifiers, including built-in function names, are case insensitive. Indentation is for readability; blocks use explicit closing keywords. Identifiers begin with a letter or underscore and continue with letters, digits, or underscores.
Assign with ASCII `<-`: `Count <- Count + 1`. Compare with `=`, `<>`, `<`, `>`, `<=`, and `>=`. Standard-library STRING comparisons compare contents, case-sensitively. Use `+`, `-`, `*`, and `/` for arithmetic; built-in `/` always returns REAL. Use `&` to join strings.
@@ -24,10 +24,13 @@ The compiler extensions TRY/CATCH/ENDTRY and THROW support anonymous exceptions
Multi-file builds use the compiler extensions INCLUDE and GLOBAL. Pass every source file on the command line and include the files whose definitions are needed. The entry file contains the sole top-level executable code, or a parameterless FUNCTION MAIN returning INTEGER; definitions alone do not form a runnable program.
-#LIB marks a library source unit. A line-leading `#` starts a compiler directive, not a comment; unknown directives are errors. Library runtime diagnostics report the first user call site. See [cpsc-keyword-lib(7)](keyword/cpsc-keyword-lib.7.md).
+PP_DEFINE and PP_UNDEF control file-local, case-sensitive preprocessor switches. PP_IFDEF and PP_IFNDEF conditionally include source up to PP_ENDIF; conditions may nest. Directives occupy their own lines, allowing leading whitespace and trailing // comments. Switches have no values and do not replace text. PP_DEFINE __LIB enables library runtime line reporting for subsequent source positions; PP_UNDEF __LIB restores ordinary behavior, including inside functions. See [cpsc-keyword-pp_define(7)](keyword/cpsc-keyword-pp_define.7.md) and [cpsc-keyword-pp_ifdef(7)](keyword/cpsc-keyword-pp_ifdef.7.md).
# KEYWORDS
+- [cpsc-keyword-pp_define(7)](keyword/cpsc-keyword-pp_define.7.md): define or remove preprocessor switches and select library code regions.
+- [cpsc-keyword-pp_ifdef(7)](keyword/cpsc-keyword-pp_ifdef.7.md): conditionally compile source according to switch definitions.
+
- [cpsc-keyword-include(7)](keyword/cpsc-keyword-include.7.md): import definitions from another compilation unit.
- [cpsc-keyword-global(7)](keyword/cpsc-keyword-global.7.md): declare shared primitive variables.
- [cpsc-keyword-constant(7)](keyword/cpsc-keyword-constant.7.md): declare a named literal with block scope.
@@ -62,7 +65,7 @@ Multi-file builds use the compiler extensions INCLUDE and GLOBAL. Pass every sou
- [cpsc-keyword-div(7)](keyword/cpsc-keyword-div.7.md): perform integer division.
- [cpsc-keyword-mod(7)](keyword/cpsc-keyword-mod.7.md): find the integer division remainder.
-Closing and auxiliary keywords are documented with their main constructs: THEN/ELSE/ENDIF with IF; OF/OTHERWISE/ENDCASE with CASE; CATCH/ENDTRY/THROW with TRY; ENDWHILE with WHILE; UNTIL with REPEAT; TO/STEP/NEXT with FOR; RETURN/RETURNS/BYVAL/BYREF and function/procedure closing keywords with FUNCTION/PROCEDURE; READ/WRITE/APPEND with OPENFILE; ARRAY/OF and INTEGER/REAL/BOOLEAN/CHAR/STRING with DECLARE; ENDGLOBAL with GLOBAL; ENDTYPE with TYPE; EXTERN_FUNCTION/EXTERN_PROCEDURE with EXTERN; PUBLIC/PRIVATE/ENDCLASS with CLASS. TRUE/FALSE are BOOLEAN literals.
+Closing and auxiliary keywords are documented with their main constructs: PP_UNDEF with PP_DEFINE; PP_IFNDEF/PP_ENDIF with PP_IFDEF; THEN/ELSE/ENDIF with IF; OF/OTHERWISE/ENDCASE with CASE; CATCH/ENDTRY/THROW with TRY; ENDWHILE with WHILE; UNTIL with REPEAT; TO/STEP/NEXT with FOR; RETURN/RETURNS/BYVAL/BYREF and function/procedure closing keywords with FUNCTION/PROCEDURE; READ/WRITE/APPEND with OPENFILE; ARRAY/OF and INTEGER/REAL/BOOLEAN/CHAR/STRING with DECLARE; ENDGLOBAL with GLOBAL; ENDTYPE with TYPE; EXTERN_FUNCTION/EXTERN_PROCEDURE with EXTERN; PUBLIC/PRIVATE/ENDCLASS with CLASS. TRUE/FALSE are BOOLEAN literals.
# STRING LIBRARY FUNCTIONS
diff --git a/doc/language/keyword/cpsc-keyword-lib.7.md b/doc/language/keyword/cpsc-keyword-lib.7.md
deleted file mode 100644
index 46af0a4..0000000
--- a/doc/language/keyword/cpsc-keyword-lib.7.md
+++ /dev/null
@@ -1,31 +0,0 @@
-% CPSC-KEYWORD-LIB(7)
-
-# NAME
-
-cpsc-keyword-lib - mark a library compilation unit
-
-# SYNOPSIS
-
-```text
-#LIB
-```
-
-# DESCRIPTION
-
-A line-leading `#` (optionally preceded by whitespace) begins a compiler directive. Only `#LIB` is supported. It marks the entire source file as library code. Compile-time diagnostics retain library locations; runtime checks use the line of the first call from user code into library code, including methods, operators, automatic constructors/destructors and EXTERN calls. Library-to-library calls preserve that line. Directives are not comments.
-
-C++ library objects may read `extern "C" int __cps_user_line`, print `[Fatal] line N: reason` to stderr and throw an anonymous integer exception for pseudocode TRY/CATCH. The global is shared by the program and is not thread-local.
-
-# EXAMPLE
-
-```text
-#LIB
-PROCEDURE Check()
- DECLARE A : ARRAY[1:1] OF INTEGER
- OUTPUT A[2]
-ENDPROCEDURE
-```
-
-# SEE ALSO
-
-[cpsc-keyword-include(7)](cpsc-keyword-include.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-pp_define.7.md b/doc/language/keyword/cpsc-keyword-pp_define.7.md
new file mode 100644
index 0000000..f48148c
--- /dev/null
+++ b/doc/language/keyword/cpsc-keyword-pp_define.7.md
@@ -0,0 +1,45 @@
+% CPSC-KEYWORD-PP_DEFINE(7)
+
+# NAME
+
+cpsc-keyword-pp_define, cpsc-keyword-pp_undef - define and remove preprocessor switches
+
+# SYNOPSIS
+
+```text
+PP_DEFINE name
+PP_UNDEF name
+```
+
+# DESCRIPTION
+
+PP_DEFINE and PP_UNDEF are compiler extensions processed before ordinary source code is tokenized. Each directive occupies its own line, optionally preceded by whitespace and followed by a // comment. Names contain ASCII letters, digits and underscores, starting with a letter or underscore. Directive keywords are uppercase; switch names are case-sensitive, unlike ordinary pseudocode identifiers.
+
+PP_DEFINE enables a switch and PP_UNDEF removes it. Defining an already defined switch or removing an undefined switch has no effect. Switches have no values, do not replace source text and use a namespace separate from ordinary identifiers. Extra arguments are errors. There are no command-line switch options.
+
+Each source file starts with no switches defined. Definitions take effect in source order and continue until removed or the file ends, including across function boundaries. INCLUDE imports symbols from independently preprocessed files; switches never pass between files. Definitions in a disabled conditional region have no effect. See [cpsc-keyword-pp_ifdef(7)](cpsc-keyword-pp_ifdef.7.md) for conditional compilation.
+
+The special switch __LIB marks subsequent source positions as library code. PP_UNDEF __LIB restores ordinary code behavior for subsequent positions; both directives may appear inside a function. Runtime checks in ordinary code report their own source line. Checks in library code use the shared user call line: calls from ordinary code update it, while calls from library code preserve it. This also covers methods, operators, EXTERN calls and automatic construction or destruction. Templates retain the modes at their original definition positions. Compile-time diagnostics always retain their original source file and line. The former #LIB syntax is rejected.
+
+C++ library objects may read `extern "C" int __cps_user_line`, print `[Fatal] line N: reason` to stderr and throw an anonymous integer exception for pseudocode TRY/CATCH. The global is shared by the program and is not thread-local.
+
+# EXAMPLE
+
+```text
+PP_DEFINE __LIB
+PROCEDURE Check()
+ DECLARE A : ARRAY[1:1] OF INTEGER
+ OUTPUT A[2]
+ENDPROCEDURE
+PP_UNDEF __LIB
+
+TRY
+ CALL Check() // The library bounds check reports this call's line.
+CATCH
+ OUTPUT "Bounds error caught"
+ENDTRY
+```
+
+# SEE ALSO
+
+[cpsc-keyword-pp_ifdef(7)](cpsc-keyword-pp_ifdef.7.md), [cpsc-keyword-include(7)](cpsc-keyword-include.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md).
diff --git a/doc/language/keyword/cpsc-keyword-pp_ifdef.7.md b/doc/language/keyword/cpsc-keyword-pp_ifdef.7.md
new file mode 100644
index 0000000..b84645d
--- /dev/null
+++ b/doc/language/keyword/cpsc-keyword-pp_ifdef.7.md
@@ -0,0 +1,45 @@
+% CPSC-KEYWORD-PP_IFDEF(7)
+
+# NAME
+
+cpsc-keyword-pp_ifdef, cpsc-keyword-pp_ifndef, cpsc-keyword-pp_endif - conditionally compile source regions
+
+# SYNOPSIS
+
+```text
+PP_IFDEF name
+ source lines
+PP_ENDIF
+
+PP_IFNDEF name
+ source lines
+PP_ENDIF
+```
+
+# DESCRIPTION
+
+PP_IFDEF, PP_IFNDEF and PP_ENDIF are compiler extensions processed before lexing and parsing. PP_IFDEF includes its region when the named switch is defined; PP_IFNDEF includes it when the switch is undefined. Both require exactly one switch name. Each must have a matching PP_ENDIF, which takes no arguments. Conditions may nest, and a region is included only when all enclosing conditions are enabled. There is no PP_ELSE or condition-expression syntax.
+
+Directives occupy their own lines, optionally preceded by whitespace and followed by // comments. Keywords and switch names are case-sensitive. Switches belong to each file independently and change in source order through [cpsc-keyword-pp_define(7)](cpsc-keyword-pp_define.7.md).
+
+Disabled ordinary source is not lexed, parsed or compiled, and its PP_DEFINE and PP_UNDEF directives have no effect. Conditional nesting is still checked, including inside disabled regions. Directive lines and disabled source preserve original line numbering for diagnostics. Preprocessor conditions do not introduce ordinary scopes or add indentation in the VS Code plugin or Bundle.
+
+# EXAMPLE
+
+```text
+PP_DEFINE Debug
+PP_IFDEF Debug
+OUTPUT "Debug enabled"
+PP_IFNDEF Quiet
+OUTPUT "Verbose debug output"
+PP_ENDIF
+PP_ENDIF
+PP_UNDEF Debug
+PP_IFNDEF Debug
+OUTPUT "Debug disabled"
+PP_ENDIF
+```
+
+# SEE ALSO
+
+[cpsc-keyword-pp_define(7)](cpsc-keyword-pp_define.7.md), [cpsc-language-overview(7)](../cpsc-language-overview.7.md).
diff --git a/plugin/AGENTS/ARCH.md b/plugin/AGENTS/ARCH.md
index eb97e63..69040af 100644
--- a/plugin/AGENTS/ARCH.md
+++ b/plugin/AGENTS/ARCH.md
@@ -114,11 +114,16 @@ only a subset of its syntax:
TextMate injection, including nested declaration expressions. Comments, strings,
fields outside methods and top-level code do not receive this scope.
- Highlighting covers CASE/ENDCASE/OTHERWISE, CLASS/ENDCLASS/PUBLIC/PRIVATE,
- CONSTANT, TEMPLATE, OVERLOAD/OPERATOR, EXTERN declarations, INCLUDE_OBJ, #LIB
- and ellipses using C++ scopes. OVERLOAD borrows a declaration modifier, #LIB
- borrows #pragma and INCLUDE_OBJ borrows #include. #LIB must occupy its own line.
+ CONSTANT, TEMPLATE, OVERLOAD/OPERATOR, EXTERN declarations, INCLUDE_OBJ
+ and ellipses using C++ scopes. OVERLOAD borrows a declaration modifier and
+ INCLUDE_OBJ borrows #include.
These rules do not add indentation support for the corresponding constructs.
SET, DATE and random-file operations remain unsupported by the compiler.
+- PP_DEFINE/PP_UNDEF/PP_IFDEF/PP_IFNDEF/PP_ENDIF highlight only at line start,
+ allowing indentation. Their line context uses C++ preprocessor keyword and
+ macro-name scopes, preserving // comments and shielding switch names from
+ ordinary keyword rules. Conditions add no indentation. Do not evaluate switches
+ in the editor or add inactive-code dimming; preprocessing belongs to the compiler.
Language defaults use real Tabs, display width 4, disabled indentation detection,
full automatic indentation, on-type formatting and native bracket pair colors.