aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-27 13:55:36 +0000
committer_ov4 <ov4@ov4.org>2026-10-27 13:55:36 +0000
commita91bd145008432fb8c5d91d94ab6f50473a277f5 (patch)
tree87af3ebcd2888e129bad9863574567e1c97bb18b
parent24f410430b33aaf1f288b83ea37e18fa959e9d3e (diff)
feat(preprocessor): add file-local conditional switches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--CMakeLists.txt2
-rw-r--r--include/cps/Lexer.h8
-rw-r--r--include/cps/Preprocessor.h19
-rw-r--r--lib/Lexer/Lexer.cc5
-rw-r--r--lib/Lexer/Preprocessor.cc92
-rw-r--r--tests/i/preprocessor/case.json6
-rw-r--r--tests/i/preprocessor/expected.txt8
-rw-r--r--tests/i/preprocessor/notes.md5
-rw-r--r--tests/i/preprocessor/peer.cps9
-rw-r--r--tests/i/preprocessor/program.cps44
-rw-r--r--tests/iii/preprocessor-directives/case.json28
-rw-r--r--tests/iii/preprocessor-directives/program.cps6
-rw-r--r--tests/iii/preprocessor-lines/case.json8
-rw-r--r--tests/iii/preprocessor-lines/program.cps7
-rw-r--r--tests/iii/preprocessor-reserved/case.json8
-rw-r--r--tests/iii/preprocessor-reserved/program.cps1
-rw-r--r--tools/driver/main.cc11
17 files changed, 262 insertions, 5 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index de94612..ae8721b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,7 +42,7 @@ set(CMAKE_CXX_STANDARD 17)
include_directories(include)
-add_library(CPSLexer STATIC lib/Lexer/Lexer.cc)
+add_library(CPSLexer STATIC lib/Lexer/Lexer.cc lib/Lexer/Preprocessor.cc)
add_library(CPSParser STATIC
lib/Parser/Parser.cc
lib/Parser/ParserExpression.cc
diff --git a/include/cps/Lexer.h b/include/cps/Lexer.h
index bec1233..05fb857 100644
--- a/include/cps/Lexer.h
+++ b/include/cps/Lexer.h
@@ -113,7 +113,13 @@ enum Token {
tok_try = -700,
tok_catch = -701,
tok_endtry = -702,
- tok_throw = -703
+ tok_throw = -703,
+
+ tok_pp_define = -710,
+ tok_pp_undef = -711,
+ tok_pp_ifdef = -712,
+ tok_pp_ifndef = -713,
+ tok_pp_endif = -714
};
class Lexer {
diff --git a/include/cps/Preprocessor.h b/include/cps/Preprocessor.h
new file mode 100644
index 0000000..ebc2806
--- /dev/null
+++ b/include/cps/Preprocessor.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <istream>
+#include <string>
+#include <vector>
+
+namespace cps {
+
+struct PreprocessedSource {
+ std::string Text;
+ // One entry per original source line, before processing that line's directive.
+ std::vector<bool> LibraryLines;
+ bool HadError = false;
+};
+
+// Switches and conditional nesting belong to this input only. Masked lines retain
+// their byte lengths and line endings so the lexer sees original source locations.
+PreprocessedSource preprocess(std::istream &Input);
+
+}
diff --git a/lib/Lexer/Lexer.cc b/lib/Lexer/Lexer.cc
index 885f432..5bf2230 100644
--- a/lib/Lexer/Lexer.cc
+++ b/lib/Lexer/Lexer.cc
@@ -14,6 +14,11 @@ using namespace cps;
namespace {
const std::unordered_map<std::string, int> Keywords = {
+ {"PP_DEFINE", tok_pp_define},
+ {"PP_UNDEF", tok_pp_undef},
+ {"PP_IFDEF", tok_pp_ifdef},
+ {"PP_IFNDEF", tok_pp_ifndef},
+ {"PP_ENDIF", tok_pp_endif},
{"CONSTANT", tok_constant},
{"INCLUDE", tok_include},
{"INCLUDE_OBJ", tok_include_obj},
diff --git a/lib/Lexer/Preprocessor.cc b/lib/Lexer/Preprocessor.cc
new file mode 100644
index 0000000..235dd4b
--- /dev/null
+++ b/lib/Lexer/Preprocessor.cc
@@ -0,0 +1,92 @@
+#include "cps/Preprocessor.h"
+#include "cps/Diagnostics.h"
+#include <string_view>
+#include <unordered_set>
+
+using namespace cps;
+
+namespace {
+bool nameStart(char C) {
+ return (C >= 'A' && C <= 'Z') || (C >= 'a' && C <= 'z') || C == '_';
+}
+
+bool nameContinue(char C) {
+ return nameStart(C) || (C >= '0' && C <= '9');
+}
+
+std::string_view trim(std::string_view Text) {
+ const auto First = Text.find_first_not_of(" \t\r\v\f");
+ if (First == std::string_view::npos) return {};
+ return Text.substr(First, Text.find_last_not_of(" \t\r\v\f") - First + 1);
+}
+}
+
+PreprocessedSource cps::preprocess(std::istream &Input) {
+ PreprocessedSource Result;
+ std::unordered_set<std::string> Switches;
+ struct Conditional { bool ParentActive; int Line; };
+ std::vector<Conditional> Conditions;
+ bool Active = true;
+ int LineNumber = 0;
+ auto Error = [&](const char *Message) {
+ sourceError(LineNumber, "%s", Message);
+ Result.HadError = true;
+ };
+ std::string Line;
+ while (std::getline(Input, Line)) {
+ ++LineNumber;
+ Result.LibraryLines.push_back(Switches.count("__LIB") != 0);
+ auto Code = trim(Line);
+ size_t WordEnd = 0;
+ while (WordEnd < Code.size() && nameContinue(Code[WordEnd])) ++WordEnd;
+ auto Word = Code.substr(0, WordEnd);
+ bool Define = Word == "PP_DEFINE", Undef = Word == "PP_UNDEF";
+ bool Ifdef = Word == "PP_IFDEF", Ifndef = Word == "PP_IFNDEF";
+ bool Endif = Word == "PP_ENDIF";
+ bool Directive = Define || Undef || Ifdef || Ifndef || Endif;
+ bool Mask = Directive || !Active;
+ if (Directive) {
+ auto Tail = Code.substr(WordEnd);
+ Tail = trim(Tail.substr(0, Tail.find("//")));
+ if (Endif) {
+ if (!Tail.empty()) Error("PP_ENDIF does not take arguments");
+ if (Conditions.empty()) Error("PP_ENDIF without matching PP_IFDEF or PP_IFNDEF");
+ else {
+ Active = Conditions.back().ParentActive;
+ Conditions.pop_back();
+ }
+ } else if (Ifdef || Ifndef || Active) {
+ size_t NameEnd = 0;
+ if (!Tail.empty() && nameStart(Tail[0])) {
+ NameEnd = 1;
+ while (NameEnd < Tail.size() && nameContinue(Tail[NameEnd])) ++NameEnd;
+ }
+ bool Valid = NameEnd != 0 && NameEnd == Tail.size();
+ if (!Valid) {
+ sourceError(LineNumber, "%.*s requires exactly one switch name",
+ static_cast<int>(Word.size()), Word.data());
+ Result.HadError = true;
+ }
+ std::string Name(Tail.substr(0, NameEnd));
+ if (Ifdef || Ifndef) {
+ Conditions.push_back({Active, LineNumber});
+ bool Defined = Switches.count(Name) != 0;
+ Active = Active && Valid && (Ifdef ? Defined : !Defined);
+ } else if (Valid) {
+ if (Define) Switches.insert(Name);
+ else Switches.erase(Name);
+ }
+ }
+ }
+ if (Mask)
+ for (char &C : Line)
+ if (C != '\r') C = ' ';
+ Result.Text += Line;
+ if (!Input.eof()) Result.Text += '\n';
+ }
+ for (const auto &Condition : Conditions) {
+ sourceError(Condition.Line, "Unterminated preprocessor conditional: expected PP_ENDIF");
+ Result.HadError = true;
+ }
+ return Result;
+}
diff --git a/tests/i/preprocessor/case.json b/tests/i/preprocessor/case.json
new file mode 100644
index 0000000..c66d109
--- /dev/null
+++ b/tests/i/preprocessor/case.json
@@ -0,0 +1,6 @@
+{
+ "sources": [
+ "program.cps",
+ "peer.cps"
+ ]
+}
diff --git a/tests/i/preprocessor/expected.txt b/tests/i/preprocessor/expected.txt
new file mode 100644
index 0000000..d537856
--- /dev/null
+++ b/tests/i/preprocessor/expected.txt
@@ -0,0 +1,8 @@
+1
+2
+3
+4
+5
+6
+7
+PP_DEFINE Quoted
diff --git a/tests/i/preprocessor/notes.md b/tests/i/preprocessor/notes.md
new file mode 100644
index 0000000..2afe007
--- /dev/null
+++ b/tests/i/preprocessor/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Review status: pending human review.
+
+The main file enables MainFlag, independently checks lowercase mainflag, and skips a disabled nested region containing invalid source and switch changes. Hidden remains undefined and MainFlag remains defined until explicitly removed. A switch named Flag does not replace the ordinary variable. Neither input sees the other's switches. Enabled branches print 1 through 7, followed by a literal containing a directive name. These are compiler extensions.
diff --git a/tests/i/preprocessor/peer.cps b/tests/i/preprocessor/peer.cps
new file mode 100644
index 0000000..bb745c3
--- /dev/null
+++ b/tests/i/preprocessor/peer.cps
@@ -0,0 +1,9 @@
+PP_DEFINE PeerFlag
+PROCEDURE Peer()
+PP_IFDEF MainFlag
+OUTPUT MissingMainFlag
+PP_ENDIF
+PP_IFDEF PeerFlag
+OUTPUT 7
+PP_ENDIF
+ENDPROCEDURE
diff --git a/tests/i/preprocessor/program.cps b/tests/i/preprocessor/program.cps
new file mode 100644
index 0000000..fe0b9ce
--- /dev/null
+++ b/tests/i/preprocessor/program.cps
@@ -0,0 +1,44 @@
+PP_DEFINE MainFlag // File-local and case-sensitive.
+PP_DEFINE MainFlag
+INCLUDE "peer.cps"
+PP_IFDEF MainFlag
+OUTPUT 1
+PP_ENDIF
+PP_IFNDEF mainflag
+OUTPUT 2
+PP_ENDIF
+PP_IFDEF PeerFlag
+OUTPUT MissingPeerFlag
+PP_ENDIF
+PP_IFDEF Absent
+"unterminated literal
+PP_DEFINE Hidden
+PP_UNDEF MainFlag
+PP_IFNDEF Other
+#INVALID
+PP_DEFINE
+PP_ENDIF
+PP_ENDIF
+PP_IFNDEF Hidden
+OUTPUT 3
+PP_ENDIF
+PP_IFDEF MainFlag
+OUTPUT 4
+PP_ENDIF
+PP_UNDEF MainFlag
+PP_UNDEF MainFlag
+PP_IFDEF MainFlag
+OUTPUT MissingMainFlag
+PP_ENDIF
+PP_IFNDEF MainFlag
+ PP_IFNDEF Hidden // Nested enabled conditions.
+OUTPUT 5
+ PP_ENDIF
+PP_ENDIF
+DECLARE Flag : INTEGER
+PP_DEFINE Flag
+Flag <- 6
+OUTPUT Flag
+CALL Peer()
+// PP_IFDEF CommentedOut
+OUTPUT "PP_DEFINE Quoted"
diff --git a/tests/iii/preprocessor-directives/case.json b/tests/iii/preprocessor-directives/case.json
new file mode 100644
index 0000000..cc0301b
--- /dev/null
+++ b/tests/iii/preprocessor-directives/case.json
@@ -0,0 +1,28 @@
+{
+ "errors": [
+ {
+ "reason": "PP_ENDIF without matching",
+ "line": 1
+ },
+ {
+ "reason": "PP_DEFINE requires exactly one switch name",
+ "line": 2
+ },
+ {
+ "reason": "PP_UNDEF requires exactly one switch name",
+ "line": 3
+ },
+ {
+ "reason": "PP_IFDEF requires exactly one switch name",
+ "line": 4
+ },
+ {
+ "reason": "PP_ENDIF does not take arguments",
+ "line": 5
+ },
+ {
+ "reason": "Unterminated preprocessor conditional.*PP_ENDIF",
+ "line": 6
+ }
+ ]
+}
diff --git a/tests/iii/preprocessor-directives/program.cps b/tests/iii/preprocessor-directives/program.cps
new file mode 100644
index 0000000..8b9bee6
--- /dev/null
+++ b/tests/iii/preprocessor-directives/program.cps
@@ -0,0 +1,6 @@
+PP_ENDIF
+PP_DEFINE
+PP_UNDEF Flag extra
+PP_IFDEF 123
+PP_ENDIF extra
+PP_IFNDEF Open
diff --git a/tests/iii/preprocessor-lines/case.json b/tests/iii/preprocessor-lines/case.json
new file mode 100644
index 0000000..c84bc94
--- /dev/null
+++ b/tests/iii/preprocessor-lines/case.json
@@ -0,0 +1,8 @@
+{
+ "errors": [
+ {
+ "reason": "Unknown variable name Unknown",
+ "line": 7
+ }
+ ]
+}
diff --git a/tests/iii/preprocessor-lines/program.cps b/tests/iii/preprocessor-lines/program.cps
new file mode 100644
index 0000000..5b21eaf
--- /dev/null
+++ b/tests/iii/preprocessor-lines/program.cps
@@ -0,0 +1,7 @@
+PP_IFDEF Missing
+"invalid skipped literal
+PP_IFNDEF Nested
+OUTPUT Unknown
+PP_ENDIF
+PP_ENDIF
+OUTPUT Unknown
diff --git a/tests/iii/preprocessor-reserved/case.json b/tests/iii/preprocessor-reserved/case.json
new file mode 100644
index 0000000..e4d76fa
--- /dev/null
+++ b/tests/iii/preprocessor-reserved/case.json
@@ -0,0 +1,8 @@
+{
+ "errors": [
+ {
+ "reason": "Expected identifier after DECLARE",
+ "line": 1
+ }
+ ]
+}
diff --git a/tests/iii/preprocessor-reserved/program.cps b/tests/iii/preprocessor-reserved/program.cps
new file mode 100644
index 0000000..be20fb2
--- /dev/null
+++ b/tests/iii/preprocessor-reserved/program.cps
@@ -0,0 +1 @@
+DECLARE PP_DEFINE : INTEGER
diff --git a/tools/driver/main.cc b/tools/driver/main.cc
index d1dda81..e3d4c28 100644
--- a/tools/driver/main.cc
+++ b/tools/driver/main.cc
@@ -1,4 +1,5 @@
#include "cps/Lexer.h"
+#include "cps/Preprocessor.h"
#include "cps/Diagnostics.h"
#include "cps/Parser.h"
#include "cps/CodeGen.h"
@@ -17,6 +18,7 @@
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <fstream>
+#include <sstream>
#include <string>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
@@ -187,13 +189,16 @@ bool parseUnits(const std::vector<std::string> &InputPaths, size_t UserInputCoun
return false;
}
cps::DiagnosticSource = InputPath;
- cps::Lexer Lex(Input);
- cps::Parser Parser(Lex);
- Programs[I] = Parser.Parse();
+ auto Source = cps::preprocess(Input);
if (Input.bad()) {
llvm::errs() << cps::diagnosticPrefix(cps::Severity::Fatal) << "cannot read source file '" << InputPath << "'\n";
return false;
}
+ if (Source.HadError) return false;
+ std::istringstream Processed(Source.Text);
+ cps::Lexer Lex(Processed);
+ cps::Parser Parser(Lex);
+ Programs[I] = Parser.Parse();
if (Lex.hadError() || Parser.hadError()) return false;
Units.push_back({InputPath, Programs[I], std::vector<bool>(InputPaths.size(), false), Lex.isLibrary()});
Units.back().Visible[I] = true;