Files
ioccc/2020/yang/cppp.cc
T

860 lines
29 KiB
C++

// CPPP stands for "C Pre-PreProcessor", so named because the output
// is meant to be fed into a real C preprocessors before they are
// useful. Not to be confused with CPP (a real C preprocessor) or
// CCCP (something else entirely).
//
// This tool is written to do primarily one thing, which is to
// evaluate files that are heavy on ifdef/ifndef and self-includes,
// and unroll those to something that is more palatable to typical
// preprocessors. To that end, it is able to handle these directives:
//
// if elif else ifdef ifndef endif define undef warning error include
//
// With lots of caveats, such as:
//
// - #if and #elif only works if the expressions are shaped like these:
// #if A % B > C
// #if A > B
// #if A
//
// - #include only works if right hand side evaluates to __FILE__,
// otherwise the directive is copied to output.
//
// - __LINE__ is not preserved. Or rather, they are copied as is.
// So if a __LINE__ moved to a different line after preprocessor
// directives are evaluated, it will take on a different value as
// opposed to whatever they would have been in the original source
// location.
//
// - #define with right-hand side or macro parameters are copied to
// output, otherwise they are evaluated and dropped. The intent is
// to allow the next preprocessor to parse these and do macro
// expansion there, since this tool doesn't do any macro expansion
// on its own.
//
// That's right, no macro expansions. The one thing that is the main job
// for most other preprocessors, we don't do that here. Don't mistake
// this utility for a real preprocessor, it's just a pre-preprocessor.
// But for the one task it is implemented to do, it does so significantly
// faster than GCC or Clang's preprocessors.
//
// Requires compilers that supports C++17 or later.
#include<ctype.h>
#include<inttypes.h>
#include<stdio.h>
#include<stdlib.h>
#include<array>
#include<string_view>
#include<string>
#include<unordered_map>
#include<unordered_set>
#include<vector>
namespace {
// Maximum depth to avoid runaway includes.
//
// GCC supports 200 and said standard requires at least 15:
// https://gcc.gnu.org/onlinedocs/cpp/Implementation-limits.html
//
// We cap our max inclusion depth at 15 so that this tool can be used
// to flag inputs that might be troublesome for other preprocessors.
static constexpr int kMaxIncludeDepth = 15;
// List of characters that passes isspace().
static constexpr char kSpace[] = " \t\r\n\f\v";
// List of characters to match start of macro parameter or value.
static constexpr char kSpaceOrLeftParen[] = " \t(";
// Table of (macro name, macro definition).
//
// Note that there is no place to store macro arguments here. This is
// intentional, since we don't do any macro expansions.
using MacroTable = std::unordered_map<std::string_view, std::string_view>;
// Preprocessor directives.
enum class Directive
{
kIf, // arg = expression
kElif, // arg = expression
kElse,
kIfDef, // arg = macro to test
kIfNDef, // arg = macro to test
kEndIf,
kDefine, // arg = macro to insert
kUndef, // arg = macro to remove
kInclude, // arg = filename or macro
kWarning, // arg = output text
kError, // arg = output text
kRawDefine, // arg = input line
kText // arg = input line
};
// A single preprocessor instruction step.
struct Operation
{
// Operation parameters.
Directive directive;
std::string_view arg;
// Index of next instruction after evaluating conditional expression.
size_t true_branch, false_branch;
};
// Debugging stats.
struct Stats
{
// Number of #include counts.
//
// Number of includes overflowing signed 32bits? It's more likely
// than you think. Although if you did observe this number going
// even above just a few hundred, you are likely going to have a
// terrible experience feeding the same file to a real preprocessor.
int64_t include_count = -1;
// Number of Operation steps executed, which is roughly the number
// of lines that we didn't skip.
int64_t operation_steps = 0;
// Maximum level of self-includes observed.
int max_include_depth = 0;
};
// Load input from opened file handle and append to output string.
static void LoadInputFromHandle(FILE *infile, std::string *text)
{
std::array<char, 1024> buffer;
for(size_t read_size;;)
{
read_size = fread(buffer.data(), 1, buffer.size(), infile);
text->append(std::string(buffer.data(), read_size));
if( read_size < buffer.size() )
break;
}
}
// Load input to memory.
static bool LoadInput(const char *file_name, std::string *text)
{
FILE *infile;
if( (infile = fopen(file_name, "rb")) == nullptr )
{
fprintf(stderr, "Error reading from %s\n", file_name);
return false;
}
LoadInputFromHandle(infile, text);
fclose(infile);
return true;
}
// Return list of strings, roughly one string per line except multiline
// comments and preprocessor directives will be coalesced as one string.
static std::vector<std::string_view> TokenizeToLines(std::string *text)
{
// State machine for parsing various regions of text.
enum class State
{
Normal,
SingleQuotedString,
DoubleQuotedString,
RawString,
SingleLineComment,
BlockComment
};
State state = State::Normal;
// List of offsets that are the start of each line.
std::vector<size_t> line_offsets = {0};
// r = offset we are reading from, w = offset we are writing to. w <= r.
//
// These indices are used to modify the input string in-place, to
// skip over uninteresting things such as backslash-newline pairs
// and single line comments.
size_t w = 0;
for(size_t r = 0; r < text->size();)
{
const char c = (*text)[r];
switch( state )
{
case State::Normal:
// Normal state, check if we are about to enter enclosed things.
if( c == '\'' )
{
// Single quoted literal.
state = State::SingleQuotedString;
(*text)[w++] = (*text)[r++];
}
else if( c == '\"' )
{
// Double quoted literal.
state = State::DoubleQuotedString;
(*text)[w++] = (*text)[r++];
}
else if( c == 'R' )
{
// Start a C++11 raw string if we saw a R"( trigram not
// preceded by what might be an identifier.
//
// Note that this is the only kind of raw string we
// support, and not the full R"token( type of raw strings.
if( r == 0 ||
(!isalnum((*text)[r - 1]) && (*text)[r - 1] != '_') )
{
// While C++11 guarantees that we can read +1 past
// the end of a string, it doesn't guarantee that we
// can read +2, so we must check the bounds here
// (unlike other places).
if( r + 2 < text->size() &&
(*text)[r + 1] == '\"' &&
(*text)[r + 2] == '(' )
{
state = State::RawString;
}
}
(*text)[w++] = (*text)[r++];
}
else if( c == '/' )
{
// Start of comments.
if( (*text)[r + 1] == '/' )
{
// Single line comments are dropped entirely.
state = State::SingleLineComment;
r += 2;
}
else if( (*text)[r + 1] == '*' )
{
// Whitespaces inside block comments are preserved, such
// that line numbers and code alignment after the block
// comments are preserved.
state = State::BlockComment;
(*text)[w++] = ' ';
(*text)[w++] = ' ';
r += 2;
}
else
{
(*text)[w++] = (*text)[r++];
}
}
else if( c == '\\' && (*text)[r + 1] == '\n' )
{
// Skip over backslash-newline pairs, thus joining backslash
// escaped line breaks. Note that this is only done in
// Normal state. We specifically want to preserve
// backslash-newline pairs inside C++11 raw strings.
r += 2;
}
else
{
// Copy other characters as is, recording offsets to
// next character just after a newline.
(*text)[w++] = (*text)[r++];
if( c == '\n' )
line_offsets.push_back(w);
}
break;
case State::SingleQuotedString:
// Skip over backslash escapes, and end single quoted
// literals on single quotes.
if( c == '\\' )
{
(*text)[w++] = (*text)[r++];
}
else if( c == '\'' )
{
state = State::Normal;
}
(*text)[w++] = (*text)[r++];
break;
case State::DoubleQuotedString:
// Skip over backslash escapes, and end double quoted
// literals on double quotes.
if( c == '\\' )
{
(*text)[w++] = (*text)[r++];
}
else if( c == '\"' )
{
state = State::Normal;
}
(*text)[w++] = (*text)[r++];
break;
case State::RawString:
// End C++11 raw strings on )" bigram.
if( c == ')' && (*text)[r + 1] == '\"' )
{
(*text)[w++] = (*text)[r++];
state = State::Normal;
}
(*text)[w++] = (*text)[r++];
break;
case State::SingleLineComment:
// Drop all single line comment contents until the trailing
// newline. Only the final newline is copied to output.
if( c == '\n' )
{
state = State::Normal;
(*text)[w++] = (*text)[r++];
line_offsets.push_back(w);
}
else
{
r++;
}
break;
case State::BlockComment:
// Replace block comments with spaces, and end comments on
// */ bigram.
if( c == '*' && (*text)[r + 1] == '/' )
{
state = State::Normal;
(*text)[w++] = ' ';
(*text)[w++] = ' ';
r += 2;
}
else
{
// If the byte we are replacing is already a space character,
// then just copy it as is. Otherwise, replace it with a
// normal whitespace. This is meant to preserve layout for
// when block comments are used within a single line. If we
// were to support __LINE__, this may also help in keeping that
// consistent, but since we don't support macro expansions of
// any kind, it doesn't matter.
//
// We don't have to worry about embedded newlines mess up line
// splitting, since line splitting is done by remembering the
// offsets of all newlines outside of all enclosed regions.
// This means something like this works:
// # /*
// */ define NEXT_LINE
if( isspace(c) )
(*text)[w++] = c;
else
(*text)[w++] = ' ';
r++;
}
break;
}
}
text->erase(w);
// Build list of lines from line offsets.
std::vector<std::string_view> lines;
if( w > 0 )
{
lines.reserve(line_offsets.size());
if( line_offsets.back() != w )
line_offsets.push_back(w);
for(size_t i = 0; i + 1 < line_offsets.size(); i++)
{
std::string_view l(*text);
l.remove_prefix(line_offsets[i]);
l.remove_suffix(l.size() - (line_offsets[i + 1] - line_offsets[i]));
lines.push_back(l);
}
}
return lines;
}
// Remove leading and trailing spaces.
static std::string_view Trim(std::string_view s)
{
const size_t l_space = s.find_first_not_of(kSpace);
if( l_space > 0 )
s.remove_prefix(l_space);
const size_t r_space = s.find_last_not_of(kSpace);
if( r_space != std::string_view::npos )
s.remove_suffix(s.size() - r_space - 1);
return s;
}
// Convert a line into our operation struct.
static Operation ParseSingleLine(std::string_view line)
{
Operation op;
op.directive = Directive::kText;
op.arg = line;
// Skip past leading space and '#'.
const size_t i = line.find_first_not_of(kSpace);
if( i == std::string_view::npos || line[i] != '#' )
return op;
const size_t j = line.find_first_not_of(kSpace, i + 1);
if( j == std::string_view::npos )
return op;
// Extract directive and optional argument.
std::string_view directive(line.substr(j));
std::string_view arg;
const size_t k = directive.find_first_of(kSpace);
if( k != std::string_view::npos )
{
arg = Trim(directive.substr(k));
directive.remove_suffix(directive.size() - k);
}
if( directive == "define" )
{
// If this is a macro with parameters or substitution value, we
// will copy the full directive (minus the leading and trailing
// space) to output on evaluation, so we will need to preserve
// the full line here.
if( arg.find_first_of(kSpaceOrLeftParen) != std::string_view::npos )
{
op.directive = Directive::kRawDefine;
op.arg = Trim(line);
}
else
{
op.directive = Directive::kDefine;
op.arg = arg;
}
}
#define MATCH_DIRECTIVE(s, o) \
else if( directive == s ) { op.directive = o; op.arg = arg; }
MATCH_DIRECTIVE("if", Directive::kIf)
MATCH_DIRECTIVE("elif", Directive::kElif)
MATCH_DIRECTIVE("else", Directive::kElse)
MATCH_DIRECTIVE("ifdef", Directive::kIfDef)
MATCH_DIRECTIVE("ifndef", Directive::kIfNDef)
MATCH_DIRECTIVE("endif", Directive::kEndIf)
MATCH_DIRECTIVE("undef", Directive::kUndef)
MATCH_DIRECTIVE("include", Directive::kInclude)
MATCH_DIRECTIVE("warning", Directive::kWarning)
MATCH_DIRECTIVE("error", Directive::kError)
#undef MATCH_DIRECTIVE
return op;
}
// Convert lines to operations.
static std::vector<Operation> ParseLines(
const std::vector<std::string_view> &input_lines)
{
std::vector<Operation> output;
output.reserve(input_lines.size());
for(const auto &line : input_lines)
output.push_back(ParseSingleLine(line));
return output;
}
// Check for nonempty stack.
template <typename T>
static void AssertNonEmptyStack(const T &stack, const char *directive)
{
if( stack.empty() )
{
fprintf(stderr, "Unmatched %s\n", directive);
exit(EXIT_FAILURE);
}
}
// Find branch locations. This makes skipping over conditional blocks
// much faster. This optimization probably doesn't make sense for
// typical code since most people aren't doing "#include __FILE__",
// but for our purposes it makes a big difference.
//
// Empirically, cppp's processing time is roughly linear to the number
// of "#include __FILE__" directives because it is able to jump to the
// relevant #if blocks quickly. Whereas GNU's cpp exhibits quadratic
// time because it always reads in the entire file for each #include.
static void IndexOps(std::vector<Operation> *code)
{
// first = list of indices looking for #else destination.
// second = list of indices looking for #endif destination.
using StackFrame = std::pair<std::vector<size_t>, std::vector<size_t>>;
std::vector<StackFrame> stack;
for(size_t i = 0; i < code->size(); i++)
{
Operation *op = &((*code)[i]);
op->true_branch = op->false_branch = i + 1;
switch( op->directive )
{
case Directive::kIf:
case Directive::kIfDef:
case Directive::kIfNDef:
// Start of conditional block.
stack.push_back(StackFrame());
stack.back().first.push_back(i);
break;
case Directive::kElif:
// End of one conditional block and start of the next block.
// Update previous false_branch destinations to point at the
// current conditional instruction.
AssertNonEmptyStack(stack, "#elif");
for(size_t f : stack.back().first)
(*code)[f].false_branch = i;
stack.back().first.clear();
stack.back().first.push_back(i);
break;
case Directive::kElse:
// End of conditional block. Update previous false_branch
// destinations to skip past current instruction directly
// into the body of the conditional branch.
AssertNonEmptyStack(stack, "#else");
for(size_t f : stack.back().first)
(*code)[f].false_branch = i + 1;
stack.back().first.clear();
break;
case Directive::kEndIf:
// End of conditional block. Update previous false_branch
// destinations to point at current instruction.
AssertNonEmptyStack(stack, "#endif");
for(size_t f : stack.back().first)
(*code)[f].false_branch = i;
for(size_t f : stack.back().second)
(*code)[f].true_branch = (*code)[f].false_branch = i;
stack.pop_back();
[[fallthrough]];
case Directive::kDefine:
case Directive::kUndef:
case Directive::kInclude:
case Directive::kWarning:
case Directive::kRawDefine:
case Directive::kText:
// Other non-branching directives, including #endif. If
// we are currently inside a conditional block and the
// next instruction ends the block, we want to update the
// current instruction later to jump to the matching #endif.
// #if ...
// #if ...
// step 1
// #endif // this will branch to #endif just before step 2
// #else
// ...
// #endif
// step 2
if( !stack.empty() &&
i + 1 < code->size() &&
((*code)[i + 1].directive == Directive::kElif ||
(*code)[i + 1].directive == Directive::kElse) )
{
stack.back().second.push_back(i);
}
break;
case Directive::kError:
// Error directives don't have any other place to go.
op->true_branch = op->false_branch = i;
break;
}
}
}
// Try to evaluate a possible macro reference as integer.
static int LookupInt(const MacroTable &table, std::string_view token)
{
std::unordered_set<std::string_view> used_macros;
for(MacroTable::const_iterator f = table.find(token); f != table.end();)
{
if( !used_macros.insert(token).second )
{
fprintf(stderr, "Recursive macro reference: %s\n",
std::string(token).c_str());
exit(EXIT_FAILURE);
}
token = f->second;
f = table.find(token);
}
int value;
if( sscanf(std::string(token).c_str(), "%d", &value) == 1 )
return value;
return 0;
}
// Evaluate a conditional expression.
static bool Evaluate(const MacroTable &table, std::string_view expr)
{
// Try matching something like "a % b > c".
const std::string expr_string(expr);
std::array<char, 64> token[5];
if( sscanf(expr_string.c_str(), "%s %s %s %s %s",
token[0].data(),
token[1].data(),
token[2].data(),
token[3].data(),
token[4].data()) == 5 )
{
const int value1 = LookupInt(table, std::string_view(token[0].data()));
const std::string_view op1(token[1].data());
const int value2 = LookupInt(table, std::string_view(token[2].data()));
const std::string_view op2(token[3].data());
const int value3 = LookupInt(table, std::string_view(token[4].data()));
#define INNER_EXPR(first_op, second_op) \
if( op2 == #second_op ) \
return value1 first_op value2 second_op value3
#define OUTER_EXPR(first_op) \
if( op1 == #first_op ) \
{ \
INNER_EXPR(first_op, ==); \
INNER_EXPR(first_op, !=); \
INNER_EXPR(first_op, <); \
INNER_EXPR(first_op, >); \
INNER_EXPR(first_op, <=); \
INNER_EXPR(first_op, >=); \
}
OUTER_EXPR(+) else
OUTER_EXPR(-) else
OUTER_EXPR(*) else
OUTER_EXPR(/) else
OUTER_EXPR(%) else
OUTER_EXPR(<<) else
OUTER_EXPR(>>)
#undef OUTER_EXPR
#undef INNER_EXPR
}
// Try matching something like "a > b";
if( sscanf(expr_string.c_str(), "%s %s %s",
token[0].data(),
token[1].data(),
token[2].data()) == 3 )
{
const int lhs = LookupInt(table, std::string_view(token[0].data()));
const std::string_view op(token[1].data());
const int rhs = LookupInt(table, std::string_view(token[2].data()));
#define EXPR(o) if( op == #o ) return lhs o rhs
EXPR(==);
EXPR(!=);
EXPR(<);
EXPR(>);
EXPR(<=);
EXPR(>=);
#undef EXPR
}
// Check if there are multiple tokens in this expression. If so,
// since it didn't match one of the earlier multi-token branches,
// it's an expression that we don't support.
//
// Also check if there are operators in this expression. This is
// a best effort attempt to issue warnings for single-token
// expressions like "x>0" (as opposed to "x > 0"), instead of
// silently evaluating them to zero.
if( expr.find_first_of(kSpace) != std::string_view::npos ||
expr.find_first_of("+-*/%^~!&|<>()") != std::string_view::npos )
{
fprintf(stderr, "Expression not supported by this preprocessor:\n%s\n",
std::string(expr).c_str());
exit(EXIT_FAILURE);
}
// Assume single value expression.
return LookupInt(table, expr) != 0;
}
// Execute preprocessor instructions.
static void Run(int depth,
const std::vector<Operation> &code,
MacroTable *macros,
Stats *stats)
{
stats->include_count++;
if( stats->max_include_depth < depth )
{
if( depth > kMaxIncludeDepth )
{
fprintf(stderr, "Include stack too deep: %d\n", depth);
exit(EXIT_FAILURE);
}
stats->max_include_depth = depth;
}
for(size_t i = 0; i < code.size();)
{
stats->operation_steps++;
const Operation &op = code[i];
i = op.true_branch;
switch( op.directive )
{
case Directive::kIf:
case Directive::kElif:
if( !Evaluate(*macros, op.arg) )
i = op.false_branch;
break;
case Directive::kIfDef:
if( macros->find(op.arg) == macros->end() )
i = op.false_branch;
break;
case Directive::kIfNDef:
if( macros->find(op.arg) != macros->end() )
i = op.false_branch;
break;
case Directive::kElse:
case Directive::kEndIf:
break;
case Directive::kDefine:
(*macros)[op.arg] = "";
break;
case Directive::kRawDefine:
{
const size_t j = op.arg.find_first_of(kSpace);
const size_t k = op.arg.find_first_not_of(kSpace, j + 1);
const size_t l = op.arg.find_first_of(kSpaceOrLeftParen, k + 1);
const std::string_view macro_name = op.arg.substr(k, l - k);
if( op.arg[l] == '(' )
{
// Macro with parameters. Since we don't do any macro
// expansion, there is no way to expand the value
// correctly, so we will mark the macro as defined but
// don't bother with putting the value in our table.
//
// That said, we will mark the macro as having *some*
// value, so we will copy #undef directives when we
// encounter those.
(*macros)[macro_name] = op.arg;
}
else
{
// Marco without parameters, but with a value. Here we
// have a chance of making some substitutions work, like
// these kind of things:
//
// #define X __FILE__
// #define Y X
// #include Y
//
// So we will record the value in our table.
const size_t value = op.arg.find_first_not_of(kSpace, l);
if( value != std::string_view::npos )
(*macros)[macro_name] = Trim(op.arg.substr(value));
else
(*macros)[macro_name] = "";
}
}
// Copy the whole macro directive to output, so that the next
// preprocessor might have some chance to make use of it.
fwrite(op.arg.data(), op.arg.size(), 1, stdout);
fputc('\n', stdout);
break;
case Directive::kUndef:
if( const MacroTable::iterator f = macros->find(op.arg);
f != macros->end() )
{
// If a macro has value, then we also want to include the
// #undef statement in the output, otherwise a subsequent
// #define for the same macro name might not work.
if( !(f->second.empty()) )
{
fputs("#undef ", stdout);
fwrite(op.arg.data(), op.arg.size(), 1, stdout);
fputc('\n', stdout);
}
macros->erase(f);
}
break;
case Directive::kInclude:
{
std::string_view include_file = op.arg;
std::unordered_set<std::string_view> used_macros;
for(auto f = macros->find(include_file); f != macros->end();)
{
if( !used_macros.insert(include_file).second )
{
fprintf(stderr, "Recursive macro reference: %s\n",
std::string(include_file).c_str());
exit(EXIT_FAILURE);
}
include_file = f->second;
f = macros->find(include_file);
}
if( include_file == "__FILE__" )
{
Run(depth + 1, code, macros, stats);
}
else
{
fputs("#include ", stdout);
fwrite(op.arg.data(), op.arg.size(), 1, stdout);
fputc('\n', stdout);
}
}
break;
case Directive::kWarning:
fwrite(op.arg.data(), op.arg.size(), 1, stderr);
fputc('\n', stderr);
break;
case Directive::kError:
fwrite(op.arg.data(), op.arg.size(), 1, stderr);
fputc('\n', stderr);
exit(EXIT_FAILURE);
break;
case Directive::kText:
fwrite(op.arg.data(), op.arg.size(), 1, stdout);
break;
}
}
}
} // namespace
int main(int argc, char **argv)
{
// Initialize macros and load input.
MacroTable macros;
std::string input_text;
int input_file_count = 0;
for(int i = 1; i < argc; i++)
{
if( argv[i][0] == '-' && argv[i][1] == 'D' )
{
const std::string_view d(argv[i] + 2);
const size_t e = d.find('=');
if( e != std::string_view::npos )
macros[d.substr(0, e)] = d.substr(e + 1);
else
macros[d] = "";
}
else
{
if( !LoadInput(argv[i], &input_text) )
return 1;
input_file_count++;
}
}
// If no input is specified, try reading from stdin.
if( input_file_count == 0 )
LoadInputFromHandle(stdin, &input_text);
// Parse lines.
std::vector<Operation> code = ParseLines(TokenizeToLines(&input_text));
IndexOps(&code);
// Execute preprocessor.
Stats stats;
Run(0, code, &macros, &stats);
printf("# /* include_count = %" PRId64 " */\n", stats.include_count);
printf("# /* operation_steps = %" PRId64 " */\n", stats.operation_steps);
printf("# /* max_include_depth = %d */\n", stats.max_include_depth);
return 0;
}