diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-27 13:55:36 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-27 13:55:36 +0000 |
| commit | a91bd145008432fb8c5d91d94ab6f50473a277f5 (patch) | |
| tree | 87af3ebcd2888e129bad9863574567e1c97bb18b | |
| parent | 24f410430b33aaf1f288b83ea37e18fa959e9d3e (diff) | |
feat(preprocessor): add file-local conditional switches
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rw-r--r-- | include/cps/Lexer.h | 8 | ||||
| -rw-r--r-- | include/cps/Preprocessor.h | 19 | ||||
| -rw-r--r-- | lib/Lexer/Lexer.cc | 5 | ||||
| -rw-r--r-- | lib/Lexer/Preprocessor.cc | 92 | ||||
| -rw-r--r-- | tests/i/preprocessor/case.json | 6 | ||||
| -rw-r--r-- | tests/i/preprocessor/expected.txt | 8 | ||||
| -rw-r--r-- | tests/i/preprocessor/notes.md | 5 | ||||
| -rw-r--r-- | tests/i/preprocessor/peer.cps | 9 | ||||
| -rw-r--r-- | tests/i/preprocessor/program.cps | 44 | ||||
| -rw-r--r-- | tests/iii/preprocessor-directives/case.json | 28 | ||||
| -rw-r--r-- | tests/iii/preprocessor-directives/program.cps | 6 | ||||
| -rw-r--r-- | tests/iii/preprocessor-lines/case.json | 8 | ||||
| -rw-r--r-- | tests/iii/preprocessor-lines/program.cps | 7 | ||||
| -rw-r--r-- | tests/iii/preprocessor-reserved/case.json | 8 | ||||
| -rw-r--r-- | tests/iii/preprocessor-reserved/program.cps | 1 | ||||
| -rw-r--r-- | tools/driver/main.cc | 11 |
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; |
