]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Replace use of cpp with the fit-to-purpose c-pp to avoid cpp's C-centric/JS-unfriendl...
authorstephan <stephan@noemail.net>
Fri, 18 Nov 2022 02:29:59 +0000 (02:29 +0000)
committerstephan <stephan@noemail.net>
Fri, 18 Nov 2022 02:29:59 +0000 (02:29 +0000)
FossilOrigin-Name: 49d70f071e918d5d095c807575bb7ce2b287a123261e789e938521b3b409429a

ext/wasm/GNUmakefile
ext/wasm/api/pre-js.js
ext/wasm/api/sqlite3-api-opfs.js
ext/wasm/c-pp.c [new file with mode: 0644]
manifest
manifest.uuid

index 5fd0f073c1c958492cc171fd744628205a9379d1..bd30d306ea8d86e1093d2ccc06d99d22bb512565 100644 (file)
@@ -235,8 +235,8 @@ $(foreach X,$(SOAP.js) $(sqlite3-worker1.js) $(sqlite3-worker1-promiser.js),\
 all: $(sqlite3-api.ext.jses)
 
 sqlite3-api.js := $(dir.tmp)/sqlite3-api.js
-sqlite3-api.pre-cpp.js := $(dir.tmp)/sqlite3-api.pre-cpp.js
-$(sqlite3-api.pre-cpp.js): $(sqlite3-api.jses) $(MAKEFILE)
+sqlite3-api.c-pp.js := $(dir.tmp)/sqlite3-api.c-pp.js
+$(sqlite3-api.c-pp.js): $(sqlite3-api.jses) $(MAKEFILE)
        @echo "Making $@..."
        @for i in $(sqlite3-api.jses); do \
                echo "/* BEGIN FILE: $$i */"; \
@@ -257,7 +257,6 @@ $(sqlite3-api-build-version.js): $(bin.version-info) $(MAKEFILE)
 ########################################################################
 # --post-js and --pre-js are emcc flags we use to append/prepend JS to
 # the generated emscripten module file.
-pre-js.pre-cpp.js := $(dir.api)/pre-js.pre-cpp.js
 pre-js.js := $(dir.tmp)/pre-js.js
 post-js.js := $(dir.tmp)/post-js.js
 post-jses := \
@@ -291,47 +290,47 @@ $(sqlite3-license-version.js): $(sqlite3.h) $(sqlite3-license-version-header.js)
    } > $@
 
 ########################################################################
-# Transform $(1) to $(2) via cpp -E -CC $(1) ...
+# Transform $(1) to $(2) via ./c-pp -f $(1) ...
 #
-# DO NOT use gcc as the preprocessor because it will emit its own
-# license header to our output because that's a comment in its
-# stdc-predef.h, which we cannot tell it to _not_ include. The only
-# workaround to that is to allow gcc -E to strip all comments. The
-# wasm build uses clang behind emcc, anyway, so we already have a
-# clang dependency. However, the clang cpp refuses to read empty
-# strings in the form '', so we have to be sure to use "" in JS code
-# for those.
+# Historical notes:
 #
-# It's tempting to build a custom mini-cpp-like binary for this
-# purpose to avoid these dependencies and quirks. Maybe we could use
-# lemon to do the heavy lifting for that, noting that we'd still need
-# to tokenize it by hand (but only lines which start with "#" or
-# backslash-continued lines, and we could treat all other lines as
-# opaque content).
+# - We first attempted to use gcc and/or clang to preprocess JS files
+#   in the same way we would normally do C files, but C-specific quirks
+#   of each makes that untennable.
 #
-# In this build we may have #ifdef's (and the like) in arbitrary input
-# JS files and we need to preprocess those before Emscripten gets
-# ahold of them. We cannot simply preprocess the resulting
-# Emscripten-generated sqlite3.js because (A) Emscripten may choke on
-# C preprocessor directives in the input and (B) Emscripten's output
-# may contain things which cpp cannot read (like single-quoted empty
-# strings: '').
-bin.cpp ?= clang
-ifneq (,$(filter ems,$(MAKECMDGOALS)))
+# - We implemented c-pp.c (the C-Minus Pre-processor) as a custom
+#   generic/file-format-agnostic preprocessor to enable us to pack
+#   code for different target builds into the same JS files. Most
+#   notably, some ES6 module (a.k.a. ESM) features cannot legally be
+#   referenced at all in non-ESM code, e.g.  the "import" and "export"
+#   keywords. This preprocessing step permits us to swap out sections
+#   of code where necessary for ESM and non-ESM (a.k.a. vanilla JS)
+#   require different implementations. The alternative to such
+#   preprocessing, would be to have separate source files for ES6
+#   builds, which would have a higher maintenance burden than c-pp.c
+#   seems likely to.
+#
+# c-pp.c was written specifically for the sqlite project's JavaScript
+# builds but is maintained as a standalone project:
+# https://fossil.wanderinghorse.net/r/c-pp
+bin.c-pp := ./c-pp
+$(bin.c-pp): c-pp.c $(sqlite3.c) $(MAKEFILE)
+       $(CC) -O0 -o $@ c-pp.c $(sqlite3.c) '-DCMPP_DEFAULT_DELIM="//#"' -I$(dir.top)
+ifneq (,$(filter esm,$(MAKECMDGOALS)))
 js.cpp.defines ?= -DSQLITE_JS_ESM
-ems: $(filter-out ems,$(MAKECMDGOALS))
+esm: $(filter-out esm,$(MAKECMDGOALS))
 else
 js.cpp.defines ?=
 endif
-define CPP_JS
+define C-PP_JS
 # $1 = X.js. $2 = output file to generate by filtering $(1) through
 # $(bin.cpp) -E -CC.
-$(2): $(1) $$(MAKEFILE)
-       $$(bin.cpp) -E -CC -undef $(js.cpp.defines) -x c $(1) | sed -e '/^#/d' > $$@
+$(2): $(1) $$(MAKEFILE) $$(bin.c-pp)
+       $$(bin.c-pp) $(js.cpp.defines) -f $(1) -o $$@
 CLEAN_FILES += $(2)
 endef
-$(eval $(call CPP_JS,$(dir.tmp)/sqlite3-api.pre-cpp.js,$(dir.tmp)/sqlite3-api.js))
-$(eval $(call CPP_JS,$(dir.api)/pre-js.js,$(dir.tmp)/pre-js.js))
+$(eval $(call C-PP_JS,$(dir.tmp)/sqlite3-api.c-pp.js,$(dir.tmp)/sqlite3-api.js))
+$(eval $(call C-PP_JS,$(dir.api)/pre-js.js,$(dir.tmp)/pre-js.js))
 # /end CPP-of-JS bits
 ########################################################################
 
index f959c330723b53a28f5063fb9e119f43d72f2995..c6d0683ff4841ab818aab99b1c228a6b68f3a784 100644 (file)
@@ -29,9 +29,9 @@ sqlite3InitModuleState.debugModule('self.location =',self.location);
    4) If none of the above apply, (prefix+path) is returned.
 */
 Module['locateFile'] = function(path, prefix) {
-#ifdef SQLITE_JS_ESM
+//#if SQLITE_JS_ESM
   return new URL(path, import.meta.url).href;
-#else
+//#else
   'use strict';
   let theFile;
   const up = this.urlParams;
@@ -51,7 +51,7 @@ Module['locateFile'] = function(path, prefix) {
     "result =", theFile
   );
   return theFile;
-#endif /* SQLITE_JS_EMS */
+//#endif /* SQLITE_JS_EMS */
 }.bind(sqlite3InitModuleState);
 
 /**
index 18582400eb0266af06fcfdccc382a6123b81ae42..e35eed64d121181f2321f865ae1ddbd47f1ad32f 100644 (file)
@@ -167,11 +167,11 @@ const installOpfsVfs = function callee(options){
       return promiseReject_(err);
     };
     const W =
-#ifdef SQLITE_JS_ESM
+//#if SQLITE_JS_ESM
     new Worker(new URL(options.proxyUri, import.meta.url));
-#else
+//#else
     new Worker(options.proxyUri);
-#endif
+//#endif
     W._originalOnError = W.onerror /* will be restored later */;
     W.onerror = function(err){
       // The error object doesn't contain any useful info when the
diff --git a/ext/wasm/c-pp.c b/ext/wasm/c-pp.c
new file mode 100644 (file)
index 0000000..881c009
--- /dev/null
@@ -0,0 +1,1525 @@
+/*
+** 2022-11-12:
+**
+** In place of a legal notice, here is a blessing:
+**
+**  * May you do good and not evil.
+**  * May you find forgiveness for yourself and forgive others.
+**  * May you share freely, never taking more than you give.
+**
+************************************************************************
+**
+** The C-minus Preprocessor: a truly minimal C-like preprocessor.
+** Why? Because C preprocessors _can_ process non-C code but generally make
+** quite a mess of it. The purpose of this application is an extremely
+** minimal preprocessor with only the most basic functionality of a C
+** preprocessor, namely:
+**
+** - Limited `#if`, where its one argument is a macro name which
+**   resolves to true if it's defined, false if it's not. Likewise,
+**   `#ifnot` is the inverse. Includes `#else` and `#elif` and
+**   `#elifnot`. Such chains are terminated with `#endif`.
+**
+** - `#define` accepts one or more arguments, the names of
+**   macros. Each one is implicitly true.
+**
+** - `#undef` undefine one or more macros.
+**
+** - `#error` treats the rest of the line as a fatal error message.
+**
+** - `#include` treats its argument as a filename token (NOT quoted,
+**   though support for quoting may be added later). Some effort is
+**   made to prevent recursive inclusion, but that support is both
+**   somewhat fragile and possibly completely unnecessary.
+**
+** - `#pragma` is in place for adding "meta-commands", but it does not
+**   yet have any concrete list of documented commands.
+**
+*  - `#stderr` outputs its file name, line number, and the remaininder
+**   of that line to stderr.
+**
+** - `#//` acts as a single-line comment, noting that there must be as
+**   space after the `//` part because `//` is (despite appearances)
+**   parsed like a keyword.
+**
+** Note that "#" above is symbolic. The keyword delimiter is
+** configurable and defaults to "##". Define CMPP_DEFAULT_DELIM to a
+** string when compiling to define the default at build-time.
+**
+** This preprocessor does no expansion of content except within the
+** bounds of its `#keywords`.
+**
+** Design note: this code makes use of sqlite3. Though not _strictly_
+** needed in order to implement it, this tool was specifically created
+** for potential use with the sqlite3 project's own JavaScript code,
+** so there's no reason not to make use of it to do some of the heavy
+** lifting. It does not require any cutting-edge sqlite3 features and
+** should be usable with any version which supports `WITHOUT ROWID`.
+**
+** Author(s):
+**
+** - Stephan Beal <https://wanderinghorse.net/home/stephan/>
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <ctype.h>
+
+#include "sqlite3.h"
+
+#if defined(_WIN32) || defined(WIN32)
+#  include <io.h>
+#  include <fcntl.h>
+#  ifndef access
+#    define access(f,m) _access((f),(m))
+#  endif
+#else
+#  include <unistd.h>
+#endif
+
+#ifndef CMPP_DEFAULT_DELIM
+#define CMPP_DEFAULT_DELIM "##"
+#endif
+
+#if 1
+#  define CMPP_NORETURN __attribute__((noreturn))
+#else
+#  define CMPP_NORETURN
+#endif
+
+/* Fatally exits the app with the given printf-style message. */
+static CMPP_NORETURN void fatalv(char const *zFmt, va_list);
+static CMPP_NORETURN void fatal(char const *zFmt, ...);
+
+/** Proxy for free(), for symmetry with cmpp_realloc(). */
+static void cmpp_free(void *p);
+/** A realloc() proxy which dies fatally on allocation error. */
+static void * cmpp_realloc(void * p, unsigned n);
+#if 0
+/** A malloc() proxy which dies fatally on allocation error. */
+static void * cmpp_malloc(unsigned n);
+#endif
+
+/*
+** If p is stdin or stderr then this is a no-op, else it is a
+** proxy for fclose(). This is a no-op if p is NULL.
+*/
+static void FILE_close(FILE *p);
+/*
+** Works like fopen() but accepts the special name "-" to mean either
+** stdin (if zMode indicates a real-only mode) or stdout. Fails
+** fatally on error.
+*/
+static FILE * FILE_open(char const *zName, const char * zMode);
+/*
+** Reads the entire contents of the given file, allocating it in a
+** buffer which gets assigned to `*pOut`. `*nOut` gets assigned the
+** length of the output buffer. Fails fatally on error.
+*/
+static void FILE_slurp(FILE *pFile, unsigned char **pOut,
+                       unsigned * nOut);
+
+/*
+** Intended to be passed an sqlite3 result code. If it's non-0
+** then it emits a fatal error message which contains both the
+** given string and the sqlite3_errmsg() from the application's
+** database instance.
+*/
+static void db_affirm_rc(int rc, const char * zMsg);
+
+/*
+** Proxy for sqlite3_str_finish() which fails fatally if that
+** routine returns NULL.
+*/
+static char * db_str_finish(sqlite3_str *s, int * n);
+/*
+** Proxy for sqlite3_str_new() which fails fatally if that
+** routine returns NULL.
+*/
+static sqlite3_str * db_str_new(void);
+
+/* Proxy for sqlite3_finalize(). */
+static void db_finalize(sqlite3_stmt *pStmt);
+/*
+** Proxy for sqlite3_step() which fails fatally if the result
+** is anything other than SQLITE_ROW or SQLITE_DONE.
+*/
+static int db_step(sqlite3_stmt *pStmt);
+/*
+** Proxy for sqlite3_bind_int() which fails fatally on error.
+*/
+static void db_bind_int(sqlite3_stmt *pStmt, int col, int val);
+#if 0
+/*
+** Proxy for sqlite3_bind_null() which fails fatally on error.
+*/
+static void db_bind_null(sqlite3_stmt *pStmt, int col);
+#endif
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error.
+*/
+static void db_bind_text(sqlite3_stmt *pStmt, int col, const char * zStr);
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error.
+*/
+static void db_bind_textn(sqlite3_stmt *pStmt, int col, const char * zStr, int len);
+#if 0
+/*
+** Proxy for sqlite3_bind_text() which fails fatally on error. It uses
+** sqlite3_str_vappendf() so supports all of its formatting options.
+*/
+static void db_bind_textv(sqlite3_stmt *pStmt, int col, const char * zFmt, ...);
+#endif
+/*
+** Proxy for sqlite3_free(), to be passed any memory which is allocated
+** by sqlite3_malloc().
+*/
+static void db_free(void *m);
+/*
+** Adds the given `#define` macro name to the list of macros, ignoring
+** any duplicates. Fails fatally on error.
+*/
+static void db_define_add(const char * zKey);
+/*
+** Returns true if the given key is already in the `#define` list,
+** else false. Fails fatally on db error.
+*/
+static int db_define_has(const char * zName);
+/*
+** Removes the given `#define` macro name from the list of
+** macros. Fails fatally on error.
+*/
+static void db_define_rm(const char * zKey);
+/*
+** Adds the given filename to the list of being-`#include`d files,
+** using the given source file name and line number of error reporting
+** purposes. If recursion is later detected.
+*/
+static void db_including_add(const char * zKey, const char * zSrc, int srcLine);
+/*
+** Adds the given dir to the list of includes. They are checked in the
+** order they are added.
+*/
+static void db_include_dir_add(const char * zKey);
+/*
+** Returns a resolved path of PREFIX+'/'+zKey, where PREFIX is one of
+** the `#include` dirs (db_include_dir_add()). If no file match is
+** found, NULL is returned. Memory must eventually be passed to
+** db_free() to free it.
+*/
+static char * db_include_search(const char * zKey);
+/*
+** Removes the given key from the `#include` list.
+*/
+static void db_include_rm(const char * zKey);
+/*
+** A proxy for sqlite3_prepare() which fails fatally on error.
+*/
+static void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...);
+
+/*
+** Opens the given file and processes its contents as c-pp, sending
+** all output to the global c-pp output channel. Fails fatally on
+** error.
+*/
+static void cmpp_process_file(const char * zName);
+
+/*
+** Returns the number newline characters between the given starting
+** point and inclusive ending point. Results are undefined if zFrom is
+** greater than zTo.
+*/
+static unsigned count_lines(unsigned char const * zFrom,
+                            unsigned char const *zTo);
+
+/*
+** Wrapper around a FILE handle.
+*/
+struct FileWrapper {
+  /* File's name. */
+  char const *zName;
+  /* FILE handle. */
+  FILE * pFile;
+  /* Where FileWrapper_slurp() stores the file's contents. */
+  unsigned char * zContent;
+  /* Size of this->zContent, as set by FileWrapper_slurp(). */
+  unsigned nContent;
+};
+typedef struct FileWrapper FileWrapper;
+#define FileWrapper_empty_m {0,0,0,0}
+static const FileWrapper FileWrapper_empty = FileWrapper_empty_m;
+
+/* Proxy for FILE_close(). */
+static void FileWrapper_close(FileWrapper * p);
+/* Proxy for FILE_open(). */
+static void FileWrapper_open(FileWrapper * p, const char * zName, const char *zMode);
+/* Proxy for FILE_slurp(). */
+static void FileWrapper_slurp(FileWrapper * p);
+
+/*
+** Outputs a printf()-formatted message to stderr.
+*/
+static void g_stderr(char const *zFmt, ...);
+/*
+** Outputs a printf()-formatted message to stderr.
+*/
+static void g_stderrv(char const *zFmt, va_list);
+#define g_debug(lvl,pfexpr)                                          \
+  if(lvl<=g.doDebug) g_stderr("%s @ %s:%d: ",g.zArgv0,__FILE__,__LINE__); \
+  if(lvl<=g.doDebug) g_stderr pfexpr
+
+void fatalv(char const *zFmt, va_list va){
+  if(zFmt && *zFmt){
+    vfprintf(stderr, zFmt, va);
+  }
+  fputc('\n', stderr);
+  exit(1);
+}
+
+void fatal(char const *zFmt, ...){
+  va_list va;
+  va_start(va, zFmt);
+  fatalv(zFmt, va);
+  va_end(va);
+}
+
+void cmpp_free(void *p){
+  free(p);
+}
+
+void * cmpp_realloc(void * p, unsigned n){
+  void * const rc = realloc(p, n);
+  if(!rc) fatal("realloc(P,%u) failed", n);
+  return rc;
+}
+
+#if 0
+void * cmpp_malloc(unsigned n){
+  void * const rc = malloc(n);
+  if(!rc) fatal("malloc(%u) failed", n);
+  return rc;
+}
+#endif
+
+FILE * FILE_open(char const *zName, const char * zMode){
+  FILE * p;
+  if('-'==zName[0] && 0==zName[1]){
+    p = strstr(zMode,"w") ? stdout : stdin;
+  }else{
+    p = fopen(zName, zMode);
+    if(!p) fatal("Cannot open file [%s] with mode [%s]", zName, zMode);
+  }
+  return p;
+}
+
+void FILE_close(FILE *p){
+  if(p && p!=stdout && p!=stderr){
+    fclose(p);
+  }
+}
+
+void FILE_slurp(FILE *pFile, unsigned char **pOut,
+                unsigned * nOut){
+  unsigned char zBuf[1024 * 8];
+  unsigned char * pDest = 0;
+  unsigned nAlloc = 0;
+  unsigned nOff = 0;
+  /* Note that this needs to be able to work on non-seekable streams,
+  ** thus we read in chunks instead of doing a single alloc and
+  ** filling it in one go. */
+  while( !feof(pFile) ){
+    size_t const n = fread(zBuf, 1, sizeof(zBuf), pFile);
+    if(n>0){
+      if(nAlloc < nOff + n + 1){
+        nAlloc = nOff + n + 1;
+        pDest = cmpp_realloc(pDest, nAlloc);
+      }
+      memcpy(pDest + nOff, zBuf, n);
+      nOff += n;
+    }
+  }
+  if(pDest) pDest[nOff] = 0;
+  *pOut = pDest;
+  *nOut = nOff;
+}
+
+void FileWrapper_close(FileWrapper * p){
+  if(p->pFile) FILE_close(p->pFile);
+  if(p->zContent) cmpp_free(p->zContent);
+  *p = FileWrapper_empty;
+}
+
+void FileWrapper_open(FileWrapper * p, const char * zName,
+                      const char * zMode){
+  FileWrapper_close(p);
+  p->pFile = FILE_open(zName, zMode);
+  p->zName = zName;
+}
+
+void FileWrapper_slurp(FileWrapper * p){
+  assert(!p->zContent);
+  assert(p->pFile);
+  FILE_slurp(p->pFile, &p->zContent, &p->nContent);
+}
+
+unsigned count_lines(unsigned char const * zFrom, unsigned char const *zTo){
+  unsigned ln = 0;
+  unsigned char const *zPos = zFrom;
+  assert(zFrom && zTo);
+  assert(zFrom <= zTo);
+  for(; zPos < zTo; ++zPos){
+    switch(*zPos){
+      case (unsigned)'\n': ++ln; break;
+      default: break;
+    }
+  }
+  return ln;
+}
+
+enum CmppParseState {
+TS_Start = 1,
+TS_If,
+TS_IfPassed,
+TS_Else,
+TS_Error
+};
+typedef enum CmppParseState CmppParseState;
+enum CmppTokenType {
+TT_Invalid = 0,
+TT_Comment,
+TT_Define,
+TT_Elif,
+TT_ElifNot,
+TT_Else,
+TT_EndIf,
+TT_Error,
+TT_If,
+TT_IfNot,
+TT_Include,
+TT_Line,
+TT_Pragma,
+TT_Stderr,
+TT_Undef
+};
+typedef enum CmppTokenType CmppTokenType;
+
+struct CmppToken {
+  CmppTokenType ttype;
+  /* Line number of this token in the source file. */
+  unsigned lineNo;
+  /* Start of the token. */
+  unsigned char const * zBegin;
+  /* One-past-the-end byte of the token. */
+  unsigned char const * zEnd;
+};
+typedef struct CmppToken CmppToken;
+#define CmppToken_empty_m {TT_Invalid,0,0,0}
+static const CmppToken CmppToken_empty = CmppToken_empty_m;
+
+/*
+** CmppLevel represents one "level" of tokenization, starting at the
+** top of the main input, incrementing once for each level of `#if`,
+** and decrementing for each `#endif`.
+*/
+typedef struct CmppLevel CmppLevel;
+struct CmppLevel {
+  unsigned short flags;
+  /*
+  ** Used for controlling which parts of an if/elif/...endif chain
+  ** should get output.
+  */
+  unsigned short skipLevel;
+  /* The token which started this level (an 'if' or 'ifnot'). */
+  CmppToken token;
+  CmppParseState pstate;
+};
+#define CmppLevel_empty_m {0U,0U,CmppToken_empty_m,TS_Start}
+static const CmppLevel CmppLevel_empty = CmppLevel_empty_m;
+enum CmppLevel_Flags {
+/* Max depth of nested `#if` constructs in a single tokenizer. */
+CmppLevel_Max = 10,
+/* Max number of keyword arguments. */
+CmppArgs_Max = 10,
+/* Flag indicating that output for a CmpLevel should be elided. */
+CmppLevel_F_ELIDE = 0x01,
+/*
+** Mask of CmppLevel::flags which are inherited when CmppLevel_push()
+** is used.
+*/
+CmppLevel_F_INHERIT_MASK = 0x01
+};
+
+typedef struct CmppTokenizer CmppTokenizer;
+typedef struct CmppKeyword CmppKeyword;
+typedef void (*cmpp_keyword_f)(CmppKeyword const * pKw, CmppTokenizer * t);
+struct CmppKeyword {
+  const char *zName;
+  unsigned nName;
+  int bTokenize;
+  CmppTokenType ttype;
+  cmpp_keyword_f xCall;
+};
+
+static CmppKeyword const * CmppKeyword_search(const char *zName);
+static void cmpp_process_keyword(CmppTokenizer * const t);
+
+/*
+** Tokenizer for c-pp input files.
+*/
+struct CmppTokenizer {
+  const char * zName;            /* Input (file) name for error reporting */
+  unsigned const char * zBegin;  /* start of input */
+  unsigned const char * zEnd;    /* one-after-the-end of input */
+  unsigned const char * zAnchor; /* start of input or end point of
+                                    previous token */
+  unsigned const char * zPos;    /* current position */
+  unsigned int lineNo;           /* line # of current pos */
+  CmppParseState pstate;
+  CmppToken token;               /* current token result */
+  struct {
+    unsigned ndx;
+    CmppLevel stack[CmppLevel_Max];
+  } level;
+  /* Args for use in cmpp_keyword_f() impls. */
+  struct {
+    CmppKeyword const * pKw;
+    int argc;
+    const unsigned char * argv[CmppArgs_Max];
+    unsigned char lineBuf[1024];
+  } args;
+};
+#define CT_level(t) (t)->level.stack[(t)->level.ndx]
+#define CT_pstate(t) CT_level(t).pstate
+#define CT_skipLevel(t) CT_level(t).skipLevel
+#define CLvl_skip(lvl) ((lvl)->skipLevel || ((lvl)->flags & CmppLevel_F_ELIDE))
+#define CT_skip(t) CLvl_skip(&CT_level(t))
+#define CmppTokenizer_empty_m {                 \
+    0,0,0,0,0,1U/*lineNo*/,                     \
+    TS_Start,                                 \
+    CmppToken_empty_m,                        \
+    {/*level*/0U,{CmppLevel_empty_m}},       \
+    {/*args*/0,0,{0},{0}}                \
+  }
+static const CmppTokenizer CmppTokenizer_empty = CmppTokenizer_empty_m;
+
+static void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n);
+/*static void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...);*/
+
+/*
+** Pushes a new level into the given tokenizer. Fails fatally if
+** it's too deep.
+*/
+static void CmppLevel_push(CmppTokenizer * const t);
+/*
+** Pops a level from the tokenizer. Fails fatally if the top
+** level is popped.
+*/
+static void CmppLevel_pop(CmppTokenizer * const t);
+/*
+** Returns the current level object.
+*/
+static CmppLevel * CmppLevel_get(CmppTokenizer * const t);
+
+/*
+** Global app state singleton. */
+static struct Global {
+  /* main()'s argv[0]. */
+  const char * zArgv0;
+  /*
+  ** Bytes of the keyword delimiter/prefix. Owned
+  ** elsewhere.
+  */
+  const char * zDelim;
+  /* Byte length of this->zDelim. */
+  unsigned short nDelim;
+  /* If true, enables certain debugging output. */
+  int doDebug;
+  /* App's db instance. */
+  sqlite3 * db;
+  /* Output channel. */
+  FileWrapper out;
+  struct {
+    sqlite3_stmt * defIns;
+    sqlite3_stmt * defDel;
+    sqlite3_stmt * defHas;
+    sqlite3_stmt * inclIns;
+    sqlite3_stmt * inclDel;
+    sqlite3_stmt * inclHas;
+    sqlite3_stmt * inclPathAdd;
+    sqlite3_stmt * inclSearch;
+  } stmt;
+} g = {
+"?",
+CMPP_DEFAULT_DELIM/*zDelim*/,
+(unsigned short) sizeof(CMPP_DEFAULT_DELIM)-1/*nDelim*/,
+0/*doDebug*/,
+0/*db*/,
+FileWrapper_empty_m/*out*/,
+{/*stmt*/
+  0/*defIns*/, 0/*defDel*/, 0/*defHas*/,
+  0/*inclIns*/, 0/*inclDel*/, 0/*inclHas*/,
+  0/*inclPathAdd*/
+}
+};
+
+
+#if 0
+/*
+** Outputs a printf()-formatted message to c-pp's global output
+** channel.
+*/
+static void g_outf(char const *zFmt, ...);
+void g_outf(char const *zFmt, ...){
+  va_list va;
+  va_start(va, zFmt);
+  vfprintf(g.out.pFile, zFmt, va);
+  va_end(va);
+}
+#endif
+
+#if 0
+/* Outputs n bytes from z to c-pp's global output channel. */
+static void g_out(void const *z, unsigned int n);
+void g_out(void const *z, unsigned int n){
+  if(1!=fwrite(z, n, 1, g.out.pFile)){
+    int const err = errno;
+    fatal("fwrite() output failed with errno #%d", err);
+  }
+}
+#endif
+
+void g_stderrv(char const *zFmt, va_list va){
+  vfprintf(stderr, zFmt, va);
+}
+
+void g_stderr(char const *zFmt, ...){
+  va_list va;
+  va_start(va, zFmt);
+  g_stderrv(zFmt, va);
+  va_end(va);
+}
+
+#if 0
+void cmpp_t_outf(CmppTokenizer * t, char const *zFmt, ...){
+  if(!CT_skip(t)){
+    va_list va;
+    va_start(va, zFmt);
+    vfprintf(g.out.pFile, zFmt, va);
+    va_end(va);
+  }
+}
+#endif
+
+void cmpp_t_out(CmppTokenizer * t, void const *z, unsigned int n){
+  if(!CT_skip(t)){
+    if(1!=fwrite(z, n, 1, g.out.pFile)){
+      int const err = errno;
+      fatal("fwrite() output failed with errno #%d", err);
+    }
+  }
+}
+
+void CmppLevel_push(CmppTokenizer * const t){
+  CmppLevel * pPrev;
+  CmppLevel * p;
+  if(t->level.ndx+1 == (unsigned)CmppLevel_Max){
+    fatal("%sif nesting level is too deep. Max=%d\n",
+          g.zDelim, CmppLevel_Max);
+  }
+  pPrev = &CT_level(t);
+  p = &t->level.stack[++t->level.ndx];
+  *p = CmppLevel_empty;
+  p->token = t->token;
+  p->flags = (CmppLevel_F_INHERIT_MASK & pPrev->flags);
+  if(CLvl_skip(pPrev)) p->flags |= CmppLevel_F_ELIDE;
+}
+
+void CmppLevel_pop(CmppTokenizer * const t){
+  if(!t->level.ndx){
+    fatal("Internal error: CmppLevel_pop() at the top of the stack");
+  }
+  t->level.stack[t->level.ndx--] = CmppLevel_empty;
+}
+
+CmppLevel * CmppLevel_get(CmppTokenizer * const t){
+  return &t->level.stack[t->level.ndx];
+}
+
+
+void db_affirm_rc(int rc, const char * zMsg){
+  if(rc){
+    fatal("Db error #%d %s: %s", rc, zMsg, sqlite3_errmsg(g.db));
+  }
+}
+
+void db_finalize(sqlite3_stmt *pStmt){
+  sqlite3_finalize(pStmt);
+}
+
+int db_step(sqlite3_stmt *pStmt){
+  int const rc = sqlite3_step(pStmt);
+  if(SQLITE_ROW!=rc && SQLITE_DONE!=rc){
+    db_affirm_rc(rc, "from db_step()");
+  }
+  return rc;
+}
+
+static sqlite3_str * db_str_new(void){
+  sqlite3_str * rc = sqlite3_str_new(g.db);
+  if(!rc) fatal("Alloc failed for sqlite3_str_new()");
+  return rc;
+}
+
+static char * db_str_finish(sqlite3_str *s, int * n){
+  int const rc = sqlite3_str_errcode(s);
+  if(rc) fatal("Error #%d from sqlite3_str_errcode()", rc);
+  if(n) *n = sqlite3_str_length(s);
+  char * z = sqlite3_str_finish(s);
+  if(!z) fatal("Alloc failed for sqlite3_str_new()");
+  return z;
+}
+
+void db_prepare(sqlite3_stmt **pStmt, const char * zSql, ...){
+  int rc;
+  sqlite3_str * str = db_str_new();
+  char * z = 0;
+  int n = 0;
+  va_list va;
+  if(!str) fatal("sqlite3_str_new() failed");
+  va_start(va, zSql);
+  sqlite3_str_vappendf(str, zSql, va);
+  va_end(va);
+  rc = sqlite3_str_errcode(str);
+  if(rc) fatal("sqlite3_str_errcode() = %d", rc);
+  z = db_str_finish(str, &n);
+  rc = sqlite3_prepare_v2(g.db, z, n, pStmt, 0);
+  if(rc) fatal("Error #%d (%s) preparing: %s",
+               rc, sqlite3_errmsg(g.db), z);
+  sqlite3_free(z);
+}
+
+void db_bind_int(sqlite3_stmt *pStmt, int col, int val){
+  int const rc = sqlite3_bind_int(pStmt, col, val);
+  db_affirm_rc(rc,"from db_bind_int()");
+}
+
+#if 0
+void db_bind_null(sqlite3_stmt *pStmt, int col){
+  int const rc = sqlite3_bind_null(pStmt, col);
+  db_affirm_rc(rc,"from db_bind_null()");
+}
+#endif
+
+void db_bind_textn(sqlite3_stmt *pStmt, int col,
+                   const char * zStr, int n){
+  int const rc = zStr
+    ? sqlite3_bind_text(pStmt, col, zStr, n, SQLITE_TRANSIENT)
+    : sqlite3_bind_null(pStmt, col);
+  db_affirm_rc(rc,"from db_bind_textn()");
+}
+
+void db_bind_text(sqlite3_stmt *pStmt, int col,
+                  const char * zStr){
+  db_bind_textn(pStmt, col, zStr, -1);
+}
+
+#if 0
+void db_bind_textv(sqlite3_stmt *pStmt, int col,
+                   const char * zFmt, ...){
+  int rc;
+  sqlite3_str * str = db_str_new();
+  int n = 0;
+  char * z;
+  va_list va;
+  va_start(va,zFmt);
+  sqlite3_str_vappendf(str, zFmt, va);
+  va_end(va);
+  z = db_str_finish(str, &n);
+  rc = sqlite3_bind_text(pStmt, col, z, n, sqlite3_free);
+  db_affirm_rc(rc,"from db_bind_textv()");
+}
+#endif
+
+void db_free(void *m){
+  sqlite3_free(m);
+}
+
+void db_define_add(const char * zKey){
+  int rc;
+  if(!g.stmt.defIns){
+    db_prepare(&g.stmt.defIns,
+               "INSERT OR REPLACE INTO def(k) VALUES(?)");
+  }
+  db_bind_text(g.stmt.defIns, 1, zKey);
+  rc = db_step(g.stmt.defIns);
+  if(SQLITE_DONE != rc){
+    db_affirm_rc(rc, "Stepping INSERT on def");
+  }
+  g_debug(2,("define: %s\n",zKey));
+  sqlite3_reset(g.stmt.defIns);
+}
+
+int db_define_has(const char * zName){
+  int rc;
+  if(!g.stmt.defHas){
+    db_prepare(&g.stmt.defHas, "SELECT 1 FROM def WHERE k=?");
+  }
+  db_bind_text(g.stmt.defHas, 1, zName);
+  rc = db_step(g.stmt.defHas);
+  if(SQLITE_ROW == rc){
+    rc = 1;
+  }else{
+    assert(SQLITE_DONE==rc);
+    rc = 0;
+  }
+  g_debug(1,("define has [%s] = %d\n",zName, rc));
+  sqlite3_clear_bindings(g.stmt.defHas);
+  sqlite3_reset(g.stmt.defHas);
+  return rc;
+}
+
+
+void db_define_rm(const char * zKey){
+  int rc;
+  int n = 0;
+  const char *zPos = zKey;
+  if(!g.stmt.defDel){
+    db_prepare(&g.stmt.defDel, "DELETE FROM def WHERE k=?");
+  }
+  for( ; *zPos && '='!=*zPos; ++n, ++zPos) {}
+  db_bind_text(g.stmt.defDel, 1, zKey);
+  rc = db_step(g.stmt.defDel);
+  if(SQLITE_DONE != rc){
+    db_affirm_rc(rc, "Stepping DELETE on def");
+  }
+  g_debug(2,("undefine: %.*s\n",n, zKey));
+  sqlite3_clear_bindings(g.stmt.defDel);
+  sqlite3_reset(g.stmt.defDel);
+}
+
+void db_including_add(const char * zKey, const char * zSrc, int srcLine){
+  int rc;
+  if(!g.stmt.inclIns){
+    db_prepare(&g.stmt.inclIns,
+               "INSERT OR FAIL INTO incl(file,srcFile,srcLine) VALUES(?,?,?)");
+  }
+  db_bind_text(g.stmt.inclIns, 1, zKey);
+  db_bind_text(g.stmt.inclIns, 2, zSrc);
+  db_bind_int(g.stmt.inclIns, 3, srcLine);
+  rc = db_step(g.stmt.inclIns);
+  if(SQLITE_DONE != rc){
+    db_affirm_rc(rc, "Stepping INSERT on incl");
+  }
+  g_debug(2,("inclpath add [%s] from [%s]:%d\n", zKey, zSrc, srcLine));
+  sqlite3_clear_bindings(g.stmt.inclIns);
+  sqlite3_reset(g.stmt.inclIns);
+}
+
+void db_include_rm(const char * zKey){
+  int rc;
+  if(!g.stmt.inclDel){
+    db_prepare(&g.stmt.inclDel, "DELETE FROM incl WHERE file=?");
+  }
+  db_bind_text(g.stmt.inclDel, 1, zKey);
+  rc = db_step(g.stmt.inclDel);
+  if(SQLITE_DONE != rc){
+    db_affirm_rc(rc, "Stepping DELETE on incl");
+  }
+  g_debug(2,("inclpath rm [%s]\n", zKey));
+  sqlite3_clear_bindings(g.stmt.inclDel);
+  sqlite3_reset(g.stmt.inclDel);
+}
+
+char * db_include_search(const char * zKey){
+  char * zName = 0;
+  if(!g.stmt.inclSearch){
+    db_prepare(&g.stmt.inclSearch,
+               "SELECT ?1 fn WHERE fileExists(fn) "
+               "UNION ALL SELECT * FROM ("
+               "SELECT replace(dir||'/'||?1, '//','/') AS fn "
+               "FROM inclpath WHERE fileExists(fn) ORDER BY seq"
+               ")");
+  }
+  db_bind_text(g.stmt.inclSearch, 1, zKey);
+  if(SQLITE_ROW==db_step(g.stmt.inclSearch)){
+    const unsigned char * z = sqlite3_column_text(g.stmt.inclSearch, 0);
+    zName = z ? sqlite3_mprintf("%s", z) : 0;
+    if(!zName) fatal("Alloc failed");
+  }
+  sqlite3_clear_bindings(g.stmt.inclSearch);
+  sqlite3_reset(g.stmt.inclSearch);
+  return zName;
+}
+
+static int db_including_has(const char * zName){
+  int rc;
+  if(!g.stmt.inclHas){
+    db_prepare(&g.stmt.inclHas, "SELECT 1 FROM incl WHERE file=?");
+  }
+  db_bind_text(g.stmt.inclHas, 1, zName);
+  rc = db_step(g.stmt.inclHas);
+  if(SQLITE_ROW == rc){
+    rc = 1;
+  }else{
+    assert(SQLITE_DONE==rc);
+    rc = 0;
+  }
+  g_debug(2,("inclpath has [%s] = %d\n",zName, rc));
+  sqlite3_clear_bindings(g.stmt.inclHas);
+  sqlite3_reset(g.stmt.inclHas);
+  return rc;
+}
+
+#if 0
+/*
+** Fails fatally if the `#include` list contains the given key.
+*/
+static void db_including_check(const char * zKey);
+void db_including_check(const char * zName){
+  if(db_including_has(zName)){
+    fatal("Recursive include detected: %s\n", zName);
+  }
+}
+#endif
+
+void db_include_dir_add(const char * zDir){
+  static int seq = 0;
+  int rc;
+  if(!g.stmt.inclPathAdd){
+    db_prepare(&g.stmt.inclPathAdd,
+               "INSERT OR FAIL INTO inclpath(seq,dir) VALUES(?,?)");
+  }
+  db_bind_int(g.stmt.inclPathAdd, 1, ++seq);
+  db_bind_text(g.stmt.inclPathAdd, 2, zDir);
+  rc = db_step(g.stmt.inclPathAdd);
+  if(SQLITE_DONE != rc){
+    db_affirm_rc(rc, "Stepping INSERT on inclpath");
+  }
+  g_debug(2,("inclpath add #%d: %s\n",seq, zDir));
+  sqlite3_clear_bindings(g.stmt.inclPathAdd);
+  sqlite3_reset(g.stmt.inclPathAdd);
+}
+
+static void cmpp_atexit(void){
+#define FINI(M) if(g.stmt.M) sqlite3_finalize(g.stmt.M)
+  FINI(defIns); FINI(defDel); FINI(defHas);
+  FINI(inclIns); FINI(inclDel); FINI(inclHas);
+  FINI(inclPathAdd); FINI(inclSearch);
+#undef FINI
+  FileWrapper_close(&g.out);
+  if(g.db) sqlite3_close(g.db);
+}
+
+/*
+** sqlite3 UDF which returns true if its argument refers to an
+** accessible file, else false.
+*/
+static void udf_file_exists(
+  sqlite3_context *context,
+  int argc,
+  sqlite3_value **argv
+){
+  const char *zName;
+  (void)(argc);  /* Unused parameter */
+  zName = (const char*)sqlite3_value_text(argv[0]);
+  if( zName==0 ) return;
+  sqlite3_result_int(context, 0==access(zName, 0));
+}
+
+/* Initialize g.db, failing fatally on error. */
+static void cmpp_initdb(void){
+  int rc;
+  char * zErr = 0;
+  const char * zSchema =
+    "CREATE TABLE def("
+      "k TEXT PRIMARY KEY NOT NULL"
+    /*"v INTEGER DEFAULT 1"*/
+    ") WITHOUT ROWID;"
+    /* ^^^ defines */
+    "CREATE TABLE incl("
+      "file TEXT PRIMARY KEY NOT NULL,"
+      "srcFile TEXT DEFAULT NULL,"
+      "srcLine INTEGER DEFAULT 0"
+    ") WITHOUT ROWID;"
+    /* ^^^ files currently being included */
+    "CREATE TABLE inclpath("
+      "seq INTEGER UNIQUE, "
+      "dir TEXT PRIMARY KEY NOT NULL ON CONFLICT IGNORE"
+    ")"
+    /* ^^^ include path */
+    ;
+  assert(0==g.db);
+  if(g.db) return;
+  rc = sqlite3_open_v2(":memory:", &g.db, SQLITE_OPEN_READWRITE, 0);
+  if(rc) fatal("Error opening :memory: db.");
+  rc = sqlite3_exec(g.db, zSchema, 0, 0, &zErr);
+  if(rc) fatal("Error initializing database: %s", zErr);
+  rc = sqlite3_create_function(g.db, "fileExists", 1, 
+                               SQLITE_UTF8|SQLITE_DIRECTONLY, 0,
+                               udf_file_exists, 0, 0);
+  db_affirm_rc(rc, "UDF registration failed.");
+}
+
+/*
+** For position zPos, which must be in the half-open range
+** [zBegin,zEnd), returns g.nDelim if it is at the start of a line and
+** starts with g.zDelim, else returns 0.
+*/
+static unsigned short cmpp_is_delim(unsigned char const *zBegin,
+                                    unsigned char const *zEnd,
+                                    unsigned char const *zPos){
+  assert(zEnd>zBegin);
+  assert(zPos<zEnd);
+  assert(zPos>=zBegin);
+  if(zPos>zBegin &&
+     ('\n'!=*(zPos - 1)
+      || ((unsigned)(zEnd - zPos) <= g.nDelim))){
+    return 0;
+  }else if(0==memcmp(zPos, g.zDelim, g.nDelim)){
+    return g.nDelim;
+  }else{
+    return 0;
+  }
+}
+
+/*
+** Scans t to the next keyword line, emitting all input before that
+** which is _not_ a keyword line unless it's elided due to being
+** inside a block which elides its content. Returns 0 if no keyword
+** line was found, in which case the end of the input has been
+** reached, else returns a truthy value and sets up t's state for use
+** with cmpp_process_keyword(), which should then be called.
+*/
+static int cmpp_next_keyword_line(CmppTokenizer * const t){
+  unsigned char const * zStart;
+  unsigned char const * z;
+  CmppToken * const tok = &t->token;
+  unsigned short isDelim = 0;
+
+  assert(t->zBegin);
+  assert(t->zEnd > t->zBegin);
+  if(!t->zPos) t->zPos = t->zBegin;
+  t->zAnchor = t->zPos;
+  zStart = z = t->zPos;
+  *tok = CmppToken_empty;
+  while(z<t->zEnd
+        && 0==(isDelim = cmpp_is_delim(t->zBegin, t->zEnd, z))){
+    ++z;
+  }
+  if(z>zStart){
+    /* We passed up content */
+    cmpp_t_out(t, zStart, (unsigned)(z - zStart));
+  }
+  assert(isDelim==0 || isDelim==g.nDelim);
+  tok->lineNo = t->lineNo += count_lines(zStart, z);
+  if(isDelim){
+    /* Handle backslash-escaped newlines */
+    int isEsc = 0, atEol = 0;
+    tok->zBegin = z+isDelim;
+    for( ++z ; z<t->zEnd && 0==atEol; ++z ){
+      switch((int)*z){
+        case (int)'\\':
+          isEsc = 0==isEsc; break;
+        case (int)'\n':
+          atEol = 0==isEsc;
+          isEsc = 0;
+          ++t->lineNo;
+          break;
+        default:
+          break;
+      }
+    }
+    tok->zEnd = atEol ? z-1 : z;
+    /* Strip leading spaces */
+    while(tok->zBegin < tok->zEnd && isspace((char)(*tok->zBegin))){
+      ++tok->zBegin;
+    }
+    tok->ttype = TT_Line;
+    g_debug(2,("Keyword @ line %u: [[[%.*s]]]\n",
+               tok->lineNo,
+               (int)(tok->zEnd-tok->zBegin), tok->zBegin));
+  }
+  t->zPos = z;
+  if(isDelim){
+    /* Split t->token into arguments for the line's keyword */
+    int i, argc = 0, prevChar = 0;
+    const unsigned tokLen = (unsigned)(tok->zEnd - tok->zBegin);
+    unsigned char * zKwd;
+    unsigned char * zEsc;
+    unsigned char * zz;
+
+    assert(TT_Line==tok->ttype);
+    if((unsigned)sizeof(t->args.lineBuf) < tokLen + 1){
+      fatal("Keyword line is unreasonably long: %.*s",
+            tokLen, tok->zBegin);
+    }else if(!tokLen){
+      fatal("Line #%u has no keyword after delimiter", tok->lineNo);
+    }
+    g_debug(2,("token @ line %u len=%u [[[%.*s]]]\n",
+               tok->lineNo, tokLen, tokLen, tok->zBegin));
+    zKwd = &t->args.lineBuf[0];
+    memcpy(zKwd, tok->zBegin, tokLen);
+    memset(zKwd + tokLen, 0, sizeof(t->args.lineBuf) - tokLen);
+    for( zEsc = 0, zz = zKwd; *zz; ++zz ){
+      /* Convert backslash-escaped newlines to whitespace */
+      switch((int)*zz){
+        case (int)'\\':
+          if(zEsc) zEsc = 0;
+          else zEsc = zz;
+          break;
+        case (int)'\n':
+          assert(zEsc && "Should not have an unescaped newline?");
+          if(zEsc==zz-1){
+            *zEsc = (unsigned char)' ';
+            /* FIXME?: memmove() lnBuf content one byte to the left here
+            ** to collapse backslash and newline into a single
+            ** byte. Also consider collapsing all leading space on the
+            ** next line. */
+          }
+          zEsc = 0;
+          *zz = (unsigned char)' ';
+          break;
+        default:
+          zEsc = 0;
+          break;
+      }
+    }
+    t->args.argv[argc++] = zKwd;
+    for( zz = zKwd; *zz; ++zz ){
+      if(isspace(*zz)){
+        *zz = 0;
+        break;
+      }
+    }
+    t->args.pKw = CmppKeyword_search((char const *)zKwd);
+    if(!t->args.pKw){
+      fatal("Unknown keyword '%s' at line %u\n", (char const *)zKwd,
+            tok->lineNo);
+    }
+    for( ++zz ; *zz && isspace(*zz); ++zz ){}
+    if(t->args.pKw->bTokenize){
+      for( ; *zz; prevChar = *zz, ++zz ){
+        /* Split string into word-shaped tokens. 
+        ** TODO ?= quoted strings, for the sake of the
+        ** #error keyword. */
+        if(isspace(*zz)){
+          assert(zz!=zKwd && "Leading space was stripped earlier.");
+          *zz = 0;
+        }else{
+          if(argc == (int)CmppArgs_Max){
+            fatal("Too many arguments @ line %u: %.*s",
+                  tok->lineNo, tokLen, tok->zBegin);
+          }else if(zz>zKwd && !prevChar){
+            t->args.argv[argc++] = zz;
+          }
+        }
+      }
+    }else{
+      /* Treat rest of line as one token */
+      if(*zz) t->args.argv[argc++] = zz;
+    }
+    tok->ttype = t->args.pKw->ttype;
+    if(g.doDebug>1){
+      for(i = 0; i < argc; ++i){
+        g_debug(0,("line %u arg #%d=%s\n",
+                   tok->lineNo, i,
+                   (char const *)t->args.argv[i]));
+      }
+    }
+    t->args.argc = argc;
+  }else{
+    t->args.pKw = 0;
+    t->args.argc = 0;
+  }
+  return isDelim;
+}
+
+static void cmpp_kwd__err_prefix(CmppKeyword const * pKw, CmppTokenizer *t,
+                                 char const *zPrefix){
+  g_stderr("%s%s%s @ %s line %u: ",
+           zPrefix ? zPrefix : "",
+           zPrefix ? ": " : "",
+           pKw->zName, t->zName, t->token.lineNo);
+}
+
+/* Internal error reporting helper for cmpp_keyword_f() impls. */
+static CMPP_NORETURN void cmpp_kwd__misuse(CmppKeyword const * pKw,
+                                           CmppTokenizer *t,
+                                           char const *zFmt, ...){
+  va_list va;
+  cmpp_kwd__err_prefix(pKw, t, "Fatal error");
+  va_start(va, zFmt);
+  fatalv(zFmt, va);
+  va_end(va);
+}
+
+/* No-op cmpp_keyword_f() impl. */
+static void cmpp_kwd_noop(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(t || pKw){/*unused*/}
+}
+
+/* #error impl. */
+static void cmpp_kwd_error(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(CT_skip(t)) return;
+  else{
+    assert(t->args.argc < 3);
+    const char *zBegin = t->args.argc>1
+      ? (const char *)t->args.argv[1] : 0;
+    cmpp_kwd__err_prefix(pKw, t, NULL);
+    fatal("%s", zBegin ? zBegin : "(no additional info)");
+  }
+}
+
+/* Impl. for #define, #undef */
+static void cmpp_kwd_define(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(CT_skip(t)) return;
+  if(t->args.argc<2){
+    cmpp_kwd__misuse(pKw, t, "Expecting one or more arguments");
+  }else{
+    int i = 1;
+    void (*func)(const char *) = TT_Define==pKw->ttype
+      ? db_define_add : db_define_rm;
+    for( ; i < t->args.argc; ++i){
+      func( (char const *)t->args.argv[i] );
+    }
+  }
+}
+
+/* Impl. for #if, #ifnot, #elif, #elifnot. */
+static void cmpp_kwd_if(CmppKeyword const * pKw, CmppTokenizer *t){
+  int buul;
+  CmppParseState tmpState = TS_Start;
+  if(t->args.argc!=2){
+    cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 argument");
+  }
+  /*g_debug(0,("%s %s level %u pstate=%d\n", pKw->zName,
+             (char const *)t->args.argv[1],
+             t->level.ndx, (int)CT_pstate(t)));*/
+  switch(pKw->ttype){
+    case TT_Elif:
+    case TT_ElifNot:
+      switch(CT_pstate(t)){
+        case TS_If: break;
+        case TS_IfPassed: CT_level(t).flags |= CmppLevel_F_ELIDE; return;
+        default: goto misuse;
+      }
+      break;
+    case TT_If:
+    case TT_IfNot:
+      CmppLevel_push(t);
+      break;
+    default:
+      cmpp_kwd__misuse(pKw, t, "Unpexected keyword token type");
+      break;
+  }
+  buul = db_define_has((char const *)t->args.argv[1]);
+  if(TT_IfNot==pKw->ttype || TT_ElifNot==pKw->ttype) buul = !buul;
+  if(buul){
+    CT_pstate(t) = tmpState = TS_IfPassed;
+    CT_skipLevel(t) = 0;    
+  }else{
+    CT_pstate(t) = TS_If /* also for TT_IfNot, TT_Elif, TT_ElifNot */;
+    CT_skipLevel(t) = 1;
+  }
+  if(TT_If==pKw->ttype || TT_IfNot==pKw->ttype){
+    unsigned const lvlIf = t->level.ndx;
+    CmppToken const lvlToken = CT_level(t).token;
+    while(cmpp_next_keyword_line(t)){
+      cmpp_process_keyword(t);
+      if(lvlIf > t->level.ndx){
+        assert(TT_EndIf == t->token.ttype);
+        break;
+      }
+      if(TS_IfPassed==tmpState){
+        tmpState = TS_Start;
+        t->level.stack[lvlIf].flags |= CmppLevel_F_ELIDE;
+      }
+    }
+    if(lvlIf <= t->level.ndx){
+      cmpp_kwd__err_prefix(pKw, t, NULL);
+      fatal("Input ended inside an unterminated %sif "
+            "opened at [%s] line %u",
+            g.zDelim, t->zName, lvlToken.lineNo);
+    }
+  }
+  return;
+  misuse:
+  cmpp_kwd__misuse(pKw, t, "'%s' used out of context",
+                   pKw->zName);
+}
+
+/* Impl. for #else. */
+static void cmpp_kwd_else(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(t->args.argc>1){
+    cmpp_kwd__misuse(pKw, t, "Expecting no arguments");
+  }
+  switch(CT_pstate(t)){
+    case TS_IfPassed: CT_skipLevel(t) = 1; break;
+    case TS_If: CT_skipLevel(t) = 0; break;
+    default:
+      cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'",
+                      pKw->zName);
+  }
+  /*g_debug(0,("else flags=0x%02x skipLevel=%u\n",
+    CT_level(t).flags, CT_level(t).skipLevel));*/
+  CT_pstate(t) = TS_Else;
+}
+
+/* Impl. for #endif. */
+static void cmpp_kwd_endif(CmppKeyword const * pKw, CmppTokenizer *t){
+  /* Maintenance reminder: we ignore all arguments after the endif
+  ** to allow for constructs like:
+  **
+  ** #endif // foo
+  **
+  ** in a manner which does not require a specific comment style */
+  switch(CT_pstate(t)){
+    case TS_Else:
+    case TS_If:
+    case TS_IfPassed:
+      break;
+    default:
+      cmpp_kwd__misuse(pKw, t, "'%s' with no matching 'if'",
+                       pKw->zName);
+  }
+  CmppLevel_pop(t);
+}
+
+/* Impl. for #include. */
+static void cmpp_kwd_include(CmppKeyword const * pKw, CmppTokenizer *t){
+  char const * zFile;
+  char * zResolved;
+  if(CT_skip(t)) return;
+  else if(t->args.argc!=2){
+    cmpp_kwd__misuse(pKw, t, "Expecting exactly 1 filename argument");
+  }
+  zFile = (const char *)t->args.argv[1];
+  if(db_including_has(zFile)){
+    /* Note that different spellings of the same filename
+    ** will elude this check, but that seems okay, as different
+    ** spellings means that we're not re-running the exact same
+    ** invocation. We might want some other form of multi-include
+    ** protection, rather than this, however. There may well be
+    ** sensible uses for recursion. */
+    cmpp_kwd__err_prefix(pKw, t, NULL);
+    fatal("Recursive include of file: %s", zFile);
+  }
+  zResolved = db_include_search(zFile);
+  if(zResolved){
+    db_including_add(zFile, t->zName, t->token.lineNo);
+    cmpp_process_file(zResolved);
+    db_include_rm(zFile);
+    db_free(zResolved);
+  }else{
+    cmpp_kwd__err_prefix(pKw, t, NULL);
+    fatal("file not found: %s", zFile);
+  }
+}
+
+/* Impl. for #pragma. */
+static void cmpp_kwd_pragma(CmppKeyword const * pKw, CmppTokenizer *t){
+  const char * zArg;
+  if(CT_skip(t)) return;
+  else if(t->args.argc!=2){
+    cmpp_kwd__misuse(pKw, t, "Expecting one argument");
+  }
+  zArg = (const char *)t->args.argv[1];
+#define M(X) 0==strcmp(zArg,X)
+  if(M("defines")){
+    sqlite3_stmt * q = 0;
+    db_prepare(&q, "SELECT k FROM def ORDER BY k");
+    g_stderr("cmpp defines:\n");
+    while(SQLITE_ROW==db_step(q)){
+      int const n = sqlite3_column_bytes(q, 0);
+      const char * z = (const char *)sqlite3_column_text(q, 0);
+      g_stderr("\t%.*s\n", n, z);
+    }
+    db_finalize(q);
+  }else{
+    cmpp_kwd__misuse(pKw, t, "Unknown pragma");
+  }
+#undef M
+}
+
+/* #stder impl. */
+static void cmpp_kwd_stderr(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(CT_skip(t)) return;
+  else{
+    const char *zBegin = t->args.argc>1
+      ? (const char *)t->args.argv[1] : 0;
+    if(zBegin){
+      g_stderr("%s:%u: %s\n", t->zName, t->token.lineNo, zBegin);
+    }else{
+      g_stderr("%s:%u: (no %.*s%s argument)\n",
+               t->zName, t->token.lineNo,
+               g.nDelim, g.zDelim, pKw->zName);
+    }
+  }
+}
+
+#if 0
+/* Impl. for dummy placeholder. */
+static void cmpp_kwd_todo(CmppKeyword const * pKw, CmppTokenizer *t){
+  if(t){/*unused*/}
+  g_debug(0,("TODO: keyword handler for %s\n", pKw->zName));
+}
+#endif
+
+CmppKeyword aKeywords[] = {
+/* Keep these sorted by zName */
+  {"//", 2, 0, TT_Comment, cmpp_kwd_noop},
+  {"define", 6, 1, TT_Define, cmpp_kwd_define},
+  {"elif", 4, 1, TT_Elif, cmpp_kwd_if},
+  {"elifnot", 7, 1, TT_ElifNot, cmpp_kwd_if},
+  {"else", 4, 1, TT_Else, cmpp_kwd_else},
+  {"endif", 5, 0, TT_EndIf, cmpp_kwd_endif},
+  {"error", 4, 0, TT_Error, cmpp_kwd_error},
+  {"if", 2, 1, TT_If, cmpp_kwd_if},
+  {"ifnot", 5, 1, TT_IfNot, cmpp_kwd_if},
+  {"include", 7, 0, TT_Include, cmpp_kwd_include},
+  {"pragma", 6, 1, TT_Pragma, cmpp_kwd_pragma},
+  {"stderr", 6, 0, TT_Stderr, cmpp_kwd_stderr},
+  {"undef", 5, 1, TT_Undef, cmpp_kwd_define},
+  {0,0,TT_Invalid, 0}
+};
+
+static int cmp_CmppKeyword(const void *p1, const void *p2){
+  char const * zName = (const char *)p1;
+  CmppKeyword const * kw = (CmppKeyword const *)p2;
+  return strcmp(zName, kw->zName);
+}
+
+CmppKeyword const * CmppKeyword_search(const char *zName){
+  return (CmppKeyword const *)bsearch(zName, &aKeywords[0],
+                                      sizeof(aKeywords)/sizeof(aKeywords[0]) - 1,
+                                      sizeof(aKeywords[0]),
+                                      cmp_CmppKeyword);
+}
+
+void cmpp_process_keyword(CmppTokenizer * const t){
+  assert(t->args.pKw);
+  assert(t->args.argc);
+  t->args.pKw->xCall(t->args.pKw, t);
+  t->args.pKw = 0;
+  t->args.argc = 0;
+}
+
+void cmpp_process_file(const char * zName){
+  FileWrapper fw = FileWrapper_empty;
+  CmppTokenizer ct = CmppTokenizer_empty;
+
+  FileWrapper_open(&fw, zName, "r");
+  FileWrapper_slurp(&fw);
+  g_debug(1,("Read %u byte(s) from [%s]\n", fw.nContent, fw.zName));
+  ct.zName = zName;
+  ct.zBegin = fw.zContent;
+  ct.zEnd = fw.zContent + fw.nContent;
+  while(cmpp_next_keyword_line(&ct)){
+    cmpp_process_keyword(&ct);
+  }
+  FileWrapper_close(&fw);
+  if(0!=ct.level.ndx){
+    CmppLevel * const lv = CmppLevel_get(&ct);
+    fatal("Input ended inside an unterminated nested construct"
+          "opened at [%s] line %u", zName, lv->token.lineNo);
+  }
+}
+
+static void usage(int isErr){
+  FILE * const fOut = isErr ? stderr : stdout;
+  fprintf(fOut,
+          "Usage: %s [flags] [infile]\n"
+          "Flags:\n",
+          g.zArgv0);
+#define arg(F,D) fprintf(fOut,"  %s\n      %s\n",F, D)
+  arg("-f|--file FILE","Read input from FILE (default=- (stdin)).\n"
+      "      Alternately, the first non-flag argument is assumed to "
+      "be the input file.");
+  arg("-o|--outfile FILE","Send output to FILE (default=- (stdout))");
+  arg("-DXYZ","Define XYZ to true");
+  arg("-UXYZ","Undefine XYZ (equivalent to false)");
+  arg("-IXYZ","Add dir XYZ to include path");
+  arg("-d|--delimiter VALUE", "Set keyword delimiter to VALUE "
+      "(default=" CMPP_DEFAULT_DELIM ")");
+#undef arg
+  fputs("",fOut);
+}
+
+int main(int argc, char const * const * argv){
+  int rc = 0;
+  int i;
+  int inclCount = 0;
+  const char * zInfile = 0;
+#define M(X) (0==strcmp(X,zArg))
+#define ISFLAG(X) else if(M(X))
+#define ISFLAG2(X,Y) else if(M(X) || M(Y))
+#define ARGVAL \
+  if(i+1>=argc) fatal("Missing value for flag '%s'", zArg);  \
+  zArg = argv[++i]
+  g.zArgv0 = argv[0];
+  atexit(cmpp_atexit);
+  cmpp_initdb();
+  for(i = 1; i < argc; ++i){
+    char const * zArg = argv[i];
+    while('-'==*zArg) ++zArg;
+    if(M("?") || M("help")) {
+      usage(0);
+      goto end;
+    }else if('D'==*zArg){
+      ++zArg;
+      if(!*zArg) fatal("Missing key for -D");
+      db_define_add(zArg);
+    }else if('U'==*zArg){
+      ++zArg;
+      if(!*zArg) fatal("Missing key for -U");
+      db_define_rm(zArg);
+    }else if('I'==*zArg){
+      ++zArg;
+      if(!*zArg) fatal("Missing directory for -I");
+      db_include_dir_add(zArg);
+      ++inclCount;
+    }
+    ISFLAG2("o","outfile"){
+      ARGVAL;
+      if(g.out.zName) fatal("Cannot use -o more than once.");
+      g.out.zName = zArg;
+    }
+    ISFLAG2("f","file"){
+      ARGVAL;
+      do_infile:
+      if(zInfile) fatal("Cannot use -i more than once.");
+      zInfile = zArg;
+    }
+    ISFLAG2("d","delimiter"){
+      ARGVAL;
+      g.zDelim = zArg;
+      g.nDelim = (unsigned short)strlen(zArg);
+      if(!g.nDelim) fatal("Keyword delimiter may not be empty.");
+    }
+    ISFLAG("debug"){
+      ++g.doDebug;
+    }else if(!zInfile){
+      goto do_infile;
+    }else{
+      fatal("Unhandled flag: %s", argv[i]);
+    }
+  }
+  if(!zInfile) zInfile = "-";
+  if(!g.out.zName) g.out.zName = "-";
+  if(!inclCount) db_include_dir_add(".");
+  FileWrapper_open(&g.out, g.out.zName, "w");
+  cmpp_process_file(zInfile);
+  FileWrapper_close(&g.out);
+  end:
+  return rc ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+#undef CT_level
+#undef CT_pstate
+#undef CT_skipLevel
+#undef CT_skip
+#undef CLvl_skip
index 6821e2b994df7c5bc3474bcbce6e56007c85d875..8b2b8bd4297e62c2e6ee4aba4c4682d3049504d9 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Merge\strunk\sinto\sjs-cpp\sbranch.
-D 2022-11-17T15:21:49.967
+C Replace\suse\sof\scpp\swith\sthe\sfit-to-purpose\sc-pp\sto\savoid\scpp's\sC-centric/JS-unfriendly\squirks.
+D 2022-11-18T02:29:59.533
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -488,7 +488,7 @@ F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3
 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04
 F ext/userauth/userauth.c 7f00cded7dcaa5d47f54539b290a43d2e59f4b1eb5f447545fa865f002fc80cb
 F ext/wasm/EXPORTED_FUNCTIONS.fiddle.in 27450c8b8c70875a260aca55435ec927068b34cef801a96205adb81bdcefc65c
-F ext/wasm/GNUmakefile 5034e5c0ebbf15479887d7253dcb2efb292906fb78d3e69701191c6257c45a9c
+F ext/wasm/GNUmakefile 3c97824ce76c7678344844e581a07ba75901a06def4d0046a2a410d9dd635a83
 F ext/wasm/README-dist.txt 2d670b426fc7c613b90a7d2f2b05b433088fe65181abead970980f0a4a75ea20
 F ext/wasm/README.md ef39861aa21632fdbca0bdd469f78f0096f6449a720f3f39642594af503030e9
 F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api 9120c2f8f51fa85f46dcf4dcb6b12f4a807d428f6089b99cdb08d8ddfcfd88b2
@@ -498,11 +498,11 @@ F ext/wasm/api/extern-post-js.js c197b7567496fc27766842f8c4f4963054bf8c667926ab3
 F ext/wasm/api/extern-pre-js.js cc61c09c7a24a07dbecb4c352453c3985170cec12b4e7e7e7a4d11d43c5c8f41
 F ext/wasm/api/post-js-footer.js cd0a8ec768501d9bd45d325ab0442037fb0e33d1f3b4f08902f15c34720ee4a1
 F ext/wasm/api/post-js-header.js d6ab3dfef4a06960d28a7eaa338d4e2a1a5981e9b38718168bbde8fdb2a439b8
-F ext/wasm/api/pre-js.js 9620327120abb15b3af96f72ef9efbcf69e78d90e501328521108b93547a8eb8
+F ext/wasm/api/pre-js.js 749bbbac2f1a2192eaba80cf5e00a3219da78b3c0a84e3019b5eef30e0bc9b88
 F ext/wasm/api/sqlite3-api-cleanup.js ecdc69dbfccfe26146f04799fcfd4a6f5790d46e7e3b9b6e9b0491f92ed8ae34
 F ext/wasm/api/sqlite3-api-glue.js 056f44b82c126358a0175e08a892d56fadfce177b0d7a0012502a6acf67ea6d5
 F ext/wasm/api/sqlite3-api-oo1.js e9a83489bbb4838ce0aee46eaaa9350e0e25a5b926b565e4f5ae8e840e4fbaed
-F ext/wasm/api/sqlite3-api-opfs.js 1def5fd676142ebe69594c77c5cf8523be1ca0880846a441bf5f981ff529672b
+F ext/wasm/api/sqlite3-api-opfs.js 4368a30586df3e11339a72082c77bdef670d619c6185c2dd1ebf5208c3ab0a5c
 F ext/wasm/api/sqlite3-api-prologue.js fd526fa017fa2578673ca18158354515c719e719a5d93f2f6d0e43f39170430e
 F ext/wasm/api/sqlite3-api-worker1.js e94ba98e44afccfa482874cd9acb325883ade50ed1f9f9526beb9de1711f182f
 F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3
@@ -513,6 +513,7 @@ F ext/wasm/api/sqlite3-worker1-promiser.js 0c7a9826dbf82a5ed4e4f7bf7816e825a52af
 F ext/wasm/api/sqlite3-worker1.js 1e54ea3d540161bcfb2100368a2fc0cad871a207b8336afee1c445715851ec54
 F ext/wasm/batch-runner.html 4deeed44fe41496dc6898d9fb17938ea3291f40f4bfb977e29d0cef96fbbe4c8
 F ext/wasm/batch-runner.js 49609e89aaac9989d6c1ad3fae268e4878e1ad7bc5fd3e5c2f44959660780b2e
+F ext/wasm/c-pp.c 92285f7bce67ed7b7020b40fde8ed0982c442b63dc33df9dfd4b658d4a6c0779
 F ext/wasm/common/SqliteTestUtil.js d8bf97ecb0705a2299765c8fc9e11b1a5ac7f10988bbf375a6558b7ca287067b
 F ext/wasm/common/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f
 F ext/wasm/common/testing.css 35889709547d89a6109ff83b25c11bbc91d8dd43aab8722e428655ca98880a06
@@ -2055,8 +2056,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 718a6d371e61359d73c8f80afdb248e3d9b4d8df4c4e5c122ac884344e31035b 7c572d02e60a83b36543ba4d9d45f61e9fc111b61fee085410c2d87558c732d6
-R dcd3d37cda21e5021973ee2a87f95321
+P e047b33d1fb7d6a32e967f03f9952249cd2da4d21dc301fe92bd7baa0da5d6a9
+R f08c58c3d67343dbb1484f5dc6d0d557
 U stephan
-Z 83259d3e3081896d2aa5795af34840f7
+Z df00239aed9bf555d5579b7e3a06d443
 # Remove this line to create a well-formed Fossil manifest.
index a3eda4f1e8a16b94f6c9cc05aa12e93b94d11b96..bef9d42ccd1059abbb6a11fa77e07bf8dd1f4c6e 100644 (file)
@@ -1 +1 @@
-e047b33d1fb7d6a32e967f03f9952249cd2da4d21dc301fe92bd7baa0da5d6a9
\ No newline at end of file
+49d70f071e918d5d095c807575bb7ce2b287a123261e789e938521b3b409429a
\ No newline at end of file