Login
shell.c at [ee03f7343f]
Login

File s2/shell.c artifact 521084cefb part of check-in ee03f7343f


/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 
/* vim: set ts=2 et sw=2 tw=80: */
/**
   s2sh: the s2 shell

   Intended uses of this code:

   1) A copy/paste bootstrap for creating new s2-using clients. To
   that end, this file "should" be commented relatively well.

   2) An extensible shell for use in running client-defined
   functionality. Extensibility comes in these forms:

   2.1) Via editing this file (discourged, as i copy it amongst
   several trees ;).

   2.2) Editing shell_extend.c and defining S2_SHELL_EXTEND when
   building this file. Preferred, as this allows clients to statically
   link in their features to the shell.

   2.3) A platform for loading new s2 features loaded via DLLs.

   2.4) provide an auto-load feature which clients can hook into to
   extend the script-side environment during shell startup.

   3) Running s2 script files in batch mode or via an interactive
   session (requires 3rd-party libs described later in these sources).


   The non-API s2 docs, including more details on the above points,
   can be found in the following public Google Doc:

   https://docs.google.com/document/d/1hvMoHSIz94dob6fCU6SLxle_s7YL6CrA_4aU12OWwWY/view


   Concepts needed to make any sense of this file:

   - cwal is a separate lib (bundled with s2) which provides
   the core-most features: Value system, memory management/lifetime,
   and garbage collector.

   - s2 is a script language built on that engine.

   Thus clients will see many mixes of the two in this code. In
   practice, most client code uses only the cwal-level APIs, but this
   app's level of code uses the s2 API relatively often as well.

*/
#if defined(NDEBUG)
/* force assert() to always work */
#  undef NDEBUG
#endif
#include <assert.h>
#if defined(S2_AMALGAMATION_BUILD)
#  include "s2_amalgamation.h"
#else
#  include "s2.h"
/* #include "s2_t10n.h" */
#endif

#if defined(S2_OS_UNIX)
#include <unistd.h> /* fork() and friends */
#include <sys/types.h> /* pid_t, if not picked up via unistd.h */
#include <errno.h> /* errno! */
#endif

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <locale.h> /* setlocale() */

/**
   Define ONE of S2_SHELL_ENABLE_LINENOISE or S2_SHELL_ENABLE_READLINE
   to enable interactive shell mode.

   GNU readline support is GNU GPL'd and requires the commonly-available
   libreadline.

   linenoise is a lower-end variation of readline with a more liberal
   license. Its sources are included in the canonical s2 source tree.

*/

#if !defined(S2_SHELL_ENABLE_LINENOISE)
#define S2_SHELL_ENABLE_LINENOISE 0
#endif

#if !defined(S2_SHELL_ENABLE_READLINE)
#define S2_SHELL_ENABLE_READLINE 0
#endif

#ifdef S2_OS_UNIX
#include <unistd.h> /* isatty() */
#endif

#if !defined(S2_SHELL_ENABLE_LINENOISE)
#define S2_SHELL_ENABLE_LINENOISE 0
#endif

#if S2_SHELL_ENABLE_LINENOISE
#  include "linenoise/linenoise.h"
#elif S2_SHELL_ENABLE_READLINE
#  include "readline/readline.h"
#  include "readline/history.h"
#endif

/*
  MARKER((...)) is an Internally-used output routine which
  includes file/line/column info.
*/
#if 1
#define MARKER(pfexp) if(1) printf("%s:%d:%s():\t",__FILE__,__LINE__,__func__); if(1) printf pfexp
#else
static void noop_printf(char const * fmt, ...) {}
#define MARKER if(0) noop_printf
#endif
#define MESSAGE(pfexp) printf pfexp
#define VERBOSE(pfexp) if(App.verboseMode) { printf("verbose: "); MESSAGE(pfexp); }(void)0

/*
  If S2_SHELL_EXTEND is defined, the client must define
  s2_shell_extend() (see shell_extend.c for one implementation) and
  link it in with this app. It will be called relatively early in the
  initialization process so that any auto-loaded scripts can make use
  of it. It must, on error, return one of the non-0 CWAL_RC_xxx error
  codes (NOT a code from outside the range! EVER!). An error is
  treated as fatal to the shell.

  See shell_extend.c for a documented example of how to use this
  approach to extending this app.

*/
#if defined(S2_SHELL_EXTEND)
extern int s2_shell_extend(s2_engine * se, int argc, char const * const * argv);
#endif

/**
   Global app-level state.
*/
static struct {
  char const * appName;
  char enableArg0Autoload;
  int enableValueRecycling;
  /**
     Max chunk size for mem chunk recycler. Set to 0 to disable.  Must
     allocate sizeof(void*) times this number, so don't set it too
     high.
  */
  cwal_size_t maxChunkCount;
  int enableStringInterning;
  int showMetrics;
  int verboseMode;
  int traceAssertions;
  int traceStacks;
  int traceSweeps;
  char const * inFile;
  char const * outFile;
  char const * const * scriptArgv;
  int scriptArgc;
  int addSweepInterval;
  int addVacuumInterval;
  /**
     Name of interactive most history file. Currently
     looks in/writes to only the current dir.
  */
  char const * lnHistoryFile;
  /**
     Default prompt string for the line-reading functions.
  */
  char const * lnPrompt;
  /**
     0=non-interactive mode, >0=interactive mode, <0=as-yet-unknown.
  */
  int interactive;
  /**
     This is the "s2" global object. It's not generally kosher to hold
     global cwal_values, but (A) we're careful in how we do so and (B)
     it simplifies a couple of our implementations.
  */
  cwal_value * s2Global;
  /**
     When shellExitFlag!=0, interactive readline looping stops.
  */
  int shellExitFlag;

  /**
     If true, do not install prototypes or global functionality.  Only
     "raw s2" is left.
   */
  int cleanroom;

  /**
     If >0, some APIs are disabled. Current levels are 0 (no
     clampdown), 1, 2 (currently the highest level).
  */
  int clampDown;
  /**
     Memory-capping configuration. Search this file for App.memcap for
     how to set it up.
  */
  cwal_memcap_config memcap;
} App = {
0/*appName*/,
1/*enableArg0Autoload*/,
1/*enableValueRecycling*/,
35/*maxChunkCount*/,
1/*enableStringInterning*/,
0/*showMetrics*/,
0/*verboseMode*/,
0/*traceAssertions*/,
0/*traceStacks*/,
0/*traceSweeps*/,
0/*inFile*/,
0/*outFile*/,
0/*scriptArgv*/,
0/*scriptArgc*/,
0/*addSweepInterval*/,
0/*addVacuumInterval*/,
"s2sh.history"/*lnHistoryFile*/,
"s2> "/*lnPrompt*/,
-1/*interactive*/,
0/*s2Global*/,
0/*shellExitFlag*/,
0/*cleanroom*/,
0/*clampDown*/,
cwal_memcap_config_empty_m/*memcap*/
};

/**
   Called by cwal_engine_init(). This does the lower-level (pre-s2)
   parts of the cwal_engine configuration. This part must not create
   any values, as s2 will destroy them right after this is called so
   that it can take over ownership of the top-most scope. Thus only
   configurable options are set here, and new functionality is added
   during the s2 part of the initialization.
*/
static int s2sh_init_engine(cwal_engine *e, cwal_engine_vtab * vtab){
  int rc = 0;
  int32_t featureFlags = 1
    ? 0
    : CWAL_FEATURE_ZERO_STRINGS_AT_CLEANUP;

  if(App.enableStringInterning){
    featureFlags |= CWAL_FEATURE_INTERN_STRINGS;
  }
  cwal_engine_feature_flags(e, featureFlags);

  {
    /* Configure the memory chunk recycler... */
    cwal_memchunk_config conf = cwal_memchunk_config_empty;
    conf.maxChunkCount = App.maxChunkCount
      /* must allocate sizeof(void*) times this
         number, so don't set it too high.
      */;
    conf.maxChunkSize = 1024 * 32;
    conf.maxTotalSize = 1024 * 256;
    conf.useForValues = 0 /* micro-optimization: true tends towards
                             sub-1% reduction in mallocs but
                             potentially has aggregate slower value
                             allocation for most cases */ ;
    rc = cwal_engine_memchunk_config(e, &conf);
    assert(!rc);
  }


#define REMAX(T,N) cwal_engine_recycle_max( e, CWAL_TYPE_ ## T, (N) )
  REMAX(UNDEF,0) /* disables recycling for all types */;
  if(App.enableValueRecycling){
#if 0
    /* Kept for historical interest... */
    /*
      Set up type-specific recycle bin sizes...

      ACHTUNG: improvements in the underlying recycler bits have made
      this form of bin size configuration "not quite correct." The
      engine consolidates like-sized value types and we have (at this level)
      no way of knowing which types it consolidates with which.
      So we're guessing...

      These don't reflect any "reserved" memory sizes, nor do they
      cause allocations. They reflect the number of objects whose
      free()'ing will likely be delayed so that its memory can be
      recycled. Thus high values here might (depending on script-side
      conditions) increase total peak memory usage (by keeping more
      "freed" values in memory at once) but does not cause more
      allocations. Contrariwise, using low recycling bin sizes may
      (depending on the script) lower peak memory usage but cause more
      total allocations. Recycling bin insertion and removal are O(1)
      operations except for strings, which have an O(N) component (N =
      best case 1, worst case 8) for removal (string re-use), so
      having large bins does not impact performance except for cleanup
      of the recycle bins at engine shutdown time (cleanup is O(N) on
      the number of bin entries). On the contrary, recycling is
      (abstractly speaking, at least) faster than reaching into the
      system allocator, and it normally speeds scripts up by a few
      ticks.
    */
    REMAX(ARRAY,30)/* s2 internally uses many short-lived arrays for
                      lifetime mgt of expression results and callback
                      function arguments.
                   */;
    REMAX(BUFFER,10)/* Does not recycle the raw buffer memory, only their
                      Value representation.
                    */;
    REMAX(DOUBLE,50)/* In practice these aren't used all that much,
                       but they're relatively expensive, so let's go
                       ahead and recycle them. An optimization made to
                       cwal on 20140823 pools the DOUBLE and INTEGER
                       recycle bins on platforms where those two types
                       have identical internal sizes (where
                       sizeof(cwal_int_t) and sizeof(cwal_double_t)
                       are the same). A side effect of that is that we
                       should set the INTEGER pool size AFTER setting
                       this one, so that that one takes precedence on
                       such platforms.
                     */;
    REMAX(EXCEPTION,3)/* There are normally not more than 1-2 of these
                         alive at any time.
                      */;
    REMAX(FUNCTION,15)/* Functions tend to stick around*/;
    REMAX(HASH,20);
    REMAX(INTEGER,80) /* These typically come and go often, but loops
                         eat through them and data-centric scripts
                         (e.g. from an sqlite db) often hold many of
                         them. See the notes for the DOUBLE entry
                         regarding combined recycle bins. */;
    REMAX(KVP,50) /* Key/value pairs - object properties (includes
                     scope-level vars) and hashtable entries.
                   */;
    REMAX(NATIVE,20)/* client-bound types (only the cwal-level
                       internals get recycled).  s2 implements the
                       PathFinder class via a Native binding.
                    */;
    REMAX(OBJECT,30)/*s2 internally uses many short-lived objects in
                      the form of scope property stores.
                    */;
    REMAX(XSTRING,20/*also Z-strings*/);
    REMAX(STRING,50)/*
                      Reminder: strings are not "as recycleable" as
                      other types because we can only recycle strings
                      if their lengths match (approximately) the new
                      string's length. String padding is used to speed
                      this up by making strings "more recyclable."
                      Only strings with a length under some officially
                      unspecified limit[1] are candidates for
                      recycling.

                      [1] = 64, as of this writing.
                    */;
    REMAX(SCOPE,4)/* scopes are, in practice, never heap allocated,
                     because their lifetime requirements so naturally
                     fit to stack-created scopes. That said, cwal
                     allows them to be heap allocated, so long as
                     their lifetime rules are _strictly_ obeyed.
                  */;
    REMAX(WEAK_REF,10)/* Not currently used by s2 or cwal internally,
                         but some client bindings may call for them.
                       */;
#else
    /* A close guess based on the post-20141129 model...  List them in
       "priority order," highest priority last.  Lower prio ones might
       get trumped by a higher prio one: they get grouped based on the
       platform's sizeof() of their concrete underlying bytes.
    */
    REMAX(UNIQUE,20)/* will end up being trumped by integer (32-bit)
                       and/or double (64-bit) */;
    REMAX(KVP,80) /* guaranteed individual recycler */;
    REMAX(WEAK_REF,30) /* guaranteed individual recycler */;
    REMAX(STRING,50) /* guaranteed individual recycler */;
    REMAX(EXCEPTION,3);
    REMAX(HASH,15) /* might also include: function, native, buffer */;
    REMAX(BUFFER,20) /* might also include: function, native, buffer, hash */ ;
    REMAX(XSTRING,20 /* also Z-strings, might also include doubles */);
    REMAX(NATIVE,20)  /* might also include: function, hash */;
    REMAX(DOUBLE,50)/* might also include z-/x-strings,
                       integer (64-bit), unique (64-bit)*/;
    REMAX(FUNCTION,50) /* might include: hash, native, buffer */;
    REMAX(ARRAY,30);
    REMAX(OBJECT,30) /* might include: buffer */;
    REMAX(INTEGER,80) /* might include: double, unique */;

#endif
  }
#undef REMAX
  /*
    Reminder to self: we cannot install functions from here because
    the s2_engine's Function prototype will not get installed by that
    time, so the functions we added would not have an 'undefined'
    prototype property. We must wait until the s2_engine is set up.

    s2 will also nuke the top scope so that it can push one of its
    own, so don't allocate any values here.
  */
  return rc;
}

/** String-is-internable predicate for cwal_engine. */
static char cstr_is_internable( void * state,
                                char const * str_,
                                cwal_size_t len ){
  enum { MaxLen = 32U };
  unsigned char const * str = (unsigned char const *)str_;
  assert(str);
  assert(len>0);
  if(len>MaxLen) return 0;
  else if(1==len){
    /*if(*str<32) return 0;
      else
    */
    return (*str<=127) ? 1 : 0;
  }
  /* else if(len<4) return 1; */
  else{
    /* Read a UTF8 identifier... */
    char const * zEnd = str_+len;
    char const * tail = str_;
    s2_read_identifier(str_, zEnd, &tail);
    if((tail>str_)
       && (len==(cwal_size_t)(tail-str_))
       ){
      /* MARKER(("internable: %.*s\n", (int)len, str_)); */
      return 1;
    }
    /* MARKER(("Not internable: %.*s\n", (int)len, str_)); */
    return 0;
  }
}

static int s2_cb_dump_val(cwal_callback_args const * args, cwal_value ** rv){
  int i;
  for( i = 0; i < args->argc; ++i ){
    /* It's be really cool if we could get the script's current location
       for use here. */
    s2_dump_val(args->argv[i], "dump_val");
  }
  *rv = cwal_value_undefined();
  return 0;
}

static void s2_dump_metrics(s2_engine * se){
  if(App.verboseMode){
    puts("");
    MESSAGE(("cwal-level allocation metrics:\n"));
    cwal_dump_allocation_metrics(se->e);
    puts("");
    if(App.verboseMode>2 && App.enableStringInterning){
      cwal_dump_interned_strings_table(se->e, 1, 32);
    }
  }
  if(se->metrics.tokenRequests){
    MESSAGE(("s2-side metrics:\n"));
    MESSAGE(("Peak cwal_scope levels: %d\n",
            se->metrics.maxScopeDepth));
    MESSAGE(("Peak eval depth: %d\n",
            se->metrics.peakSubexpDepth));
    MESSAGE(("Total s2_stokens (sizeof=%u) requested=%u, "
            "allocated=%u (=%u bytes), alive=%u, peakAlive=%u, "
            "recyclerSize=%d\n",
            (unsigned)sizeof(s2_stoken), se->metrics.tokenRequests,
            se->metrics.tokenAllocs, (unsigned)(se->metrics.tokenAllocs * (unsigned)sizeof(s2_stoken)),
            se->metrics.liveTokenCount, se->metrics.peakLiveTokenCount,
            se->recycler.stok.size));
    MESSAGE(("Total calls to s2_next_token(): %u\n",
            se->metrics.nextTokenCalls));
    if(se->metrics.tokenAllocs < se->metrics.tokenRequests){
      MESSAGE(("Saved %u bytes and %u allocs via stack token recycling :D.\n",
              (unsigned)((se->metrics.tokenRequests * (unsigned)sizeof(s2_stoken))
                         - (se->metrics.tokenAllocs * (unsigned)sizeof(s2_stoken))),
              se->metrics.tokenRequests - se->metrics.tokenAllocs
              ));
    }
  }
  if(se->metrics.funcStateAllocs){
    MESSAGE(("Allocated state for %u of %u script-side function(s) "
             "(sizeof %u), using %u bytes.\n",
             se->metrics.funcStateAllocs,
             se->metrics.funcStateRequests,
             (unsigned)sizeof(s2_func_state),
             se->metrics.funcStateMemory));
  }
  if(se->metrics.assertionCount
     && (App.showMetrics || App.traceAssertions)
     ){
    MESSAGE(("script-side 'assert' checks: %u\n",
            se->metrics.assertionCount));
  }
}

static int s2_cb_dump_metrics(cwal_callback_args const * args, cwal_value ** rv){
  s2_engine * se = s2_engine_from_args(args);
  int const oldVerbosity = App.verboseMode;
  if(args->argc && cwal_value_get_bool(args->argv[0])){
    App.verboseMode = 10;
  }
  s2_dump_metrics(se);
  App.verboseMode = oldVerbosity;
  return 0;
}

/**
   General-purpose result reporting function. rc should be the rc from
   the function we got *rv from. This function takes over
   responsibility of *rv from the caller. rv may be 0, as may *rv.

   After reporting the error, s2_engine_sweep() is run, which _might_
   clean up *rv (along with any other temporary values in the current
   scope).
*/
static int s2sh_report_result(s2_engine * se, int rc, cwal_value **rv ){
  char const * label = "result";
  cwal_value * rvTmp = 0;
  char showedMessage = 0;
  if(!rv) rv = &rvTmp;
  switch(rc){
    case 0: break;
    case CWAL_RC_RETURN:
      *rv = cwal_propagating_take(se->e);
      assert(*rv);
      break;
    case CWAL_RC_EXCEPTION:
      label = "EXCEPTION";
      *rv = cwal_exception_get(se->e);
      cwal_value_ref(*rv);
      cwal_exception_set(se->e, 0);
      cwal_value_unhand(*rv);
      assert(*rv);
      cwal_props_sort(*rv);
      break;
    case CWAL_RC_EXIT:
      rc = 0;
      *rv = s2_propagating_take(se);
      break;
    case CWAL_RC_BREAK:
      label = "UNHANDLED BREAK";
      *rv = s2_propagating_take(se);
      break;
    case CWAL_RC_CONTINUE:
      label = "UNHANDLED CONTINUE";
      *rv = 0;
      break;
    case CWAL_RC_FATAL:
      label = "FATAL";
      *rv = s2_propagating_take(se);
      assert(*rv);
      /* fall through... */
    default:
        if(rc && se && se->e && se->err.code){
          char const * msg = 0;
          int serc = 0;
          serc = s2_engine_err_get(se, &msg, 0);
          showedMessage = 1;
          if(se->err.line>0){
            fprintf(stderr,
                    "rc=%d (%s): s2_engine says error #%d (%s)"
                    " @ script [%s], line %d, column %d: %s\n",
                    rc, s2_rc_cstr(rc), serc, s2_rc_cstr(serc),
                    (char const *)se->err.script.mem,
                    se->err.line, se->err.col, msg);
          }else{
            fprintf(stderr,
                    "rc=%d (%s): s2_engine says error #%d (%s)%s %s\n",
                    rc, s2_rc_cstr(rc), serc, s2_rc_cstr(serc),
                    (msg && *msg) ? ": " : "",
                    (msg && *msg) ? msg : "");
          }
        }
        break;
  }
  if(*rv){
    assert((cwal_value_scope(*rv) || cwal_value_is_builtin(*rv))
           && "Seems like we've cleaned up too early.");
    if(rc || App.verboseMode){
      if(rc && !showedMessage){
        fprintf(stderr,"rc=%d (%s)\n", rc, s2_rc_cstr(rc));
      }
      s2_dump_value(*rv, label, 0, 0);
    }
    /* We are uncertain of v's origin but want to clean it up if
       possible, so... */
#if 1
    /*
      Disable this and use -W to see more cleanup in action.  This
      ref/unref combo ensures that if it's a temporary, it gets
      cleaned up by this unref, but if it's not, then we effectively
      do nothing to it (because someone else is holding/containing it,
      so we've only adjusted its refcount, as opposed to potentially
      shifting it around its owning scope's management lists).
    */
    cwal_refunref(*rv);
#endif
    *rv = 0;
  }
  cwal_exception_set(se->e, 0);
  s2_propagating_set(se, 0);
  s2_engine_sweep(se);
  return rc;
}

#if S2_SHELL_ENABLE_LINENOISE || S2_SHELL_ENABLE_READLINE
#define MAYBE_STATIC static
#else
#define MAYBE_STATIC
#endif

MAYBE_STATIC char * s2sh_line_read(char const * prompt){
#if S2_SHELL_ENABLE_LINENOISE
  return linenoise(prompt);
#elif S2_SHELL_ENABLE_READLINE
  return readline(prompt);
#else
  return 0;
#endif
}

MAYBE_STATIC void s2sh_line_free(char * line){
  free(line);
}

MAYBE_STATIC int s2sh_history_load(char const * fname){
#if S2_SHELL_ENABLE_LINENOISE
  return linenoiseHistoryLoad(fname);
#elif S2_SHELL_ENABLE_READLINE
  return read_history(fname);
#else
  return CWAL_RC_UNSUPPORTED;
#endif
}

MAYBE_STATIC int s2sh_history_save(char const * fname){
#if S2_SHELL_ENABLE_LINENOISE
  return linenoiseHistorySave(fname);
#elif S2_SHELL_ENABLE_READLINE
  return write_history(fname);
#else
  return CWAL_RC_UNSUPPORTED;
#endif
}

MAYBE_STATIC int s2sh_history_add(char const * line){
  assert(line);
#if S2_SHELL_ENABLE_LINENOISE
  linenoiseHistoryAdd(line);
  return 0;
#elif S2_SHELL_ENABLE_READLINE
  add_history(line);
  return 0;
#else
  return CWAL_RC_UNSUPPORTED;
#endif
}

#undef MAYBE_STATIC


static int s2_cb_s2sh_line_read(cwal_callback_args const * args, cwal_value ** rv){
  char const * prompt;
  char * line;
  if(args->argc){
    if(!(prompt = cwal_value_get_cstr(args->argv[0], 0))){
      return s2_cb_throw(args, CWAL_RC_MISUSE,
                         "Expecting a string or no arguments.");
    }
  }else{
    prompt = "prompt >" /* todo: configurable, somehow */;
  }
  if(!(line = s2sh_line_read(prompt))){
    *rv = cwal_value_undefined();
    return 0;
  }else{
    *rv = cwal_new_string_value(args->engine, line, cwal_strlen(line));
    s2sh_line_free(line);
    /* if we knew with absolute certainty that args->engine is configured
       for the same allocator as the readline bits, we could use
       a Z-string and save a copy. But we don't, generically speaking,
       even though it's likely to be the same in all but the weirdest
       configurations.
    */
    return *rv ? 0 : CWAL_RC_OOM;
  }
}

/**
   Callback implementation for s2_cb_s2sh_history_(load|add|save)().
*/
static int s2_cb_s2sh_history_las(cwal_callback_args const * args,
                                  cwal_value ** rv,
                                  char const * descr,
                                  int (*func)(char const * str) ){
  char const * str = args->argc
    ? cwal_value_get_cstr(args->argv[0], 0)
    : 0;
  if(!str){
    return s2_cb_throw(args, CWAL_RC_MISUSE,
                       "Expecting a string argument.");
  }
  else{
    int rc;
    if((rc = (*func)(str))){
      rc = s2_cb_throw(args, rc,
                       "Native %s op failed with code %d.",
                       descr);
    }
    return rc;
  }
}

static int s2_cb_s2sh_history_save(cwal_callback_args const * args, cwal_value ** rv){
  return s2_cb_s2sh_history_las( args, rv, "history-save", s2sh_history_save );
}


static int s2_cb_s2sh_history_load(cwal_callback_args const * args, cwal_value ** rv){
  return s2_cb_s2sh_history_las( args, rv, "history-load", s2sh_history_load );
}

static int s2_cb_s2sh_history_add(cwal_callback_args const * args, cwal_value ** rv){
  return s2_cb_s2sh_history_las( args, rv, "history-add", s2sh_history_add );
}


static int s2_cb_s2sh_exit(cwal_callback_args const * args, cwal_value ** rv){
  App.shellExitFlag = 1;
  *rv = cwal_value_undefined();
  return 0;
}

static int s2_install_shell_api( s2_engine * se, cwal_value * tgt,
                                 char const * name ){
  cwal_value * v;
  cwal_value * sub;
  int rc;
  if(!se || !tgt) return CWAL_RC_MISUSE;
  else if(!cwal_props_can(tgt)) return CWAL_RC_TYPE;

  if(name && *name){
    sub = cwal_new_object_value(se->e);
    if(!sub) return CWAL_RC_OOM;
    if( (rc = cwal_prop_set(tgt, name, cwal_strlen(name), sub)) ){
      cwal_value_unref(sub);
      return rc;
    }
  }else{
    sub = tgt;
  }

#define CHECKV if(!v) return CWAL_RC_OOM
#define RC if(rc) return rc
#define SET(NAME)                                              \
  CHECKV;                                                      \
  rc = cwal_prop_set( sub, NAME, cwal_strlen(NAME), v );            \
  if(rc) { cwal_value_unref(v); return rc; }(void)0

#define FUNC2(NAME,FP)                              \
  v = s2_new_function2( se, FP );                   \
  SET(NAME)

  FUNC2("historyAdd", s2_cb_s2sh_history_add);
  FUNC2("historyLoad", s2_cb_s2sh_history_load);
  FUNC2("historySave", s2_cb_s2sh_history_save);
  FUNC2("readLine", s2_cb_s2sh_line_read);
  FUNC2("exit", s2_cb_s2sh_exit);
  v = cwal_new_string_value(se->e,
#ifdef S2_OS_WINDOWS
                            "\\",
#else
                            "/",
#endif
                            1 );

#undef FUNC2
#undef CHECKV
#undef RC
#undef SET
  return 0;
}



static int s2sh_interactive(s2_engine * se){
#if !S2_SHELL_ENABLE_LINENOISE && !S2_SHELL_ENABLE_READLINE
  int rc = s2_engine_err_set(se, CWAL_RC_UNSUPPORTED,
                             "This shell was built without "
                             "line editing support.");
  s2sh_report_result(se, rc, 0);
  if(0){
    /* squelch "unused static function" warning in this build.*/
    s2_install_shell_api(0,0,0);
  }
  return rc;
#else
  char * line = 0;
  int rc = 0;

  if(!App.cleanroom){
    assert(App.s2Global);
    if((rc = s2_install_shell_api(se, App.s2Global, "shell"))){
      return rc;
    }

    rc = cwal_var_decl( se->e, 0, "g", 1,
                        cwal_new_object_value(se->e), CWAL_VAR_F_CONST );
    if(rc) return rc;
  }
  s2sh_history_load(App.lnHistoryFile) /* ignore errors */;
  
  cwal_outputf(se->e, "s2 interactive shell. All commands "
               "run in the current scope. Use your platform's EOF "
               "sequence on an empty line to exit. (Ctrl-D on Unix, "
               "Ctrl-Z(?) on Windows.) Ctrl-C might work, too.\n\n");
  if(!App.cleanroom){
    cwal_outputf(se->e, "The 'g' object is at your service as a place to "
                 "store things.\n");
  }

  while( !App.shellExitFlag && (line = s2sh_line_read(App.lnPrompt)) ){
    cwal_value * v = 0;
    s2sh_history_add(line);
    rc = s2_eval_cstr( se, 0, "shell input", line, cwal_strlen(line), &v );
    s2sh_line_free(line);
    s2sh_report_result(se, rc, &v);
    s2_engine_err_reset(se);
    rc = 0;
  }
  s2sh_history_save(App.lnHistoryFile);
  s2_engine_sweep(se);
  cwal_output(se->e, "\n", 1);
  cwal_output_flush(se->e);
  return rc;
#endif
}

static int s2sh_run_file(s2_engine * se, char const * filename){
  cwal_value * xrv = 0;
  int rc = s2_eval_filename(se, 1, filename, -1, &xrv);
  return s2sh_report_result(se, rc, &xrv);
}

#if 0
cwal_value * s2_shell_global(){
  return App.s2Global;
}
#endif

static int s2_setup_s2_global( s2_engine * se ){
  int rc = 0;
  cwal_value * v;
  cwal_engine * e = se->e;
  cwal_value * c = 0;
#define VCHECK if(!v){rc = CWAL_RC_OOM; goto end;} (void)0
#define RC if(rc) goto end

  if(App.cleanroom){
    /* Skip any other infrastructure */
    VERBOSE(("Clean-room mode is enabled: NOT installing globals/prototypes.\n"));
    goto end;
  }

  /*
    Reminder to self: we're not "leaking" anything on error here - it
    will be cleaned up by the scope when it pops during cleanup right
    after the error code is returned.
  */

  v = cwal_new_function_value( e, s2_cb_print, 0, 0, 0 );
  VCHECK;
  rc = cwal_var_decl(e, 0, "print", 5, v, 0);
  RC;
  if(App.interactive>0){
    /* just an experiment (breaks unit tests) */
    if(0){
      cwal_container_flags_set(v, CWAL_CONTAINER_DISALLOW_PROP_SET);
    }else if(1){
      cwal_prop_set(v, "x", 1, cwal_value_true());
      cwal_container_flags_set(v, CWAL_CONTAINER_DISALLOW_NEW_PROPERTIES);
    }
  }

#if 0
  /* Add a global named "$client" which is reserved as a client-side
     place to store stuff...
  */
  v = cwal_new_object_value(e);
  VCHECK;
  rc = cwal_var_decl(e, 0, "$client", 7, v, CWAL_VAR_F_CONST);
  RC;
#endif

  /* Global s2 object... */
  c = v = cwal_new_object_value(e);
  VCHECK;
  rc = cwal_var_decl(e, 0, "s2", 2, v, CWAL_VAR_F_CONST);
  RC;
  App.s2Global = v;

  if((rc = s2_install_json(se, c, "json"))) goto end;
  else if((rc = s2_install_pf(se, c))) goto end;
  else if((rc = s2_install_ob_2(se, c, "ob"))) goto end;
  else if((rc = s2_install_time(se, c, "time"))) goto end;
  else if((rc = s2_install_io(se, c, "io"))) goto end;

  rc = cwal_prop_set( c, "Buffer", 6, s2_prototype_buffer(se) );
  RC;
  rc = cwal_prop_set( c, "Hash", 4, s2_prototype_hash(se) );
  RC;

  {
    s2_func_def const funcs[] = {
      S2_FUNC2("dumpMetrics", s2_cb_dump_metrics),
      S2_FUNC2("dumpVal", s2_cb_dump_val),
      S2_FUNC2("isCallable", s2_cb_is_callable),
      S2_FUNC2("isDerefable", s2_cb_is_derefable),
      S2_FUNC2("getenv", s2_cb_getenv),
      S2_FUNC2("getResultCodeHash", s2_cb_rc_hash),
      S2_FUNC2("import", s2_cb_import_script),
      S2_FUNC2("loadModule", s2_cb_module_load),
      S2_FUNC2("tmpl", s2_cb_tmpl_to_code),
      S2_FUNC2("fork", s2_cb_fork),
      S2_FUNC2("newUnique", s2_cb_new_unique),
      s2_func_def_empty_m
    };
    if( (rc = s2_install_functions(se, c, funcs, 0)) ) goto end;
  }


  if(App.scriptArgc){
    v = 0;
    rc = cwal_parse_argv_flags(e, App.scriptArgc, App.scriptArgv, &v);
    if(!rc && v) rc = cwal_prop_set(c, "ARGV", 4, v);
    if(rc){
      if(v) cwal_value_unref(v);
      goto end;
    }
  }

#if 0
  /*
    EXPERIMENTAL: i've always avoided making scope-owned variable
    storage directly visible to script code, but we're going to
    try this...

    Add global const __GLOBAL which refers directly to the variable
    storage of the top-most scope.

    If we like this, we'll change it to a keyword, which will make it
    many times faster to process.
  */
  v = cwal_scope_properties(cwal_scope_top(cwal_scope_current_get(e)))
    /* Cannot fail: we've already declared vars in this scope, so it's
       already allocated. */;
  assert(cwal_value_get_object(v));
  assert(cwal_value_refcount(v)>0 && "Because its owning scope holds a ref.");
  assert(0==cwal_value_prototype_get(e, v) && "Because cwal does not install prototypes for these!");
  cwal_value_prototype_set( v, s2_prototype_object(se) );
  rc = cwal_var_decl(e, 0, "__GLOBAL", 8, v, CWAL_VAR_F_CONST)
    /* that effectively does: global.__GLOBAL = global */;
  RC;
#endif

#undef FUNC2
#undef VCHECK
#undef RC
  end:
  return rc;
}

static int s2sh_main2( s2_engine * se ){
  int rc = 0;

#if 0
  /* just testing post-init reconfig */
  {
    /* Configure the memory chunk recycler... */
    cwal_memchunk_config conf = cwal_memchunk_config_empty;
    conf.maxChunkCount = 0;
    conf.maxChunkSize = 1024 * 32;
    conf.maxTotalSize = 1024 * 256;
    conf.useForValues = 0;
    rc = cwal_engine_memchunk_config(se->e, &conf);
    assert(!rc);
  }
#endif

  if(App.inFile && *App.inFile){
    rc = s2sh_run_file(se, App.inFile);
    cwal_output_flush(se->e);
    /* s2_engine_sweep(se); */
  }
  if(!rc && App.interactive>0){
    rc = s2sh_interactive(se);
  }
  return rc;
}

/**
   If App.enableArg0Autoload and a file named $S2SH_INIT_SCRIPT or
   (failing that) the same as App.appName, minus any ".exe" or ".EXE"
   extension, but with an ".s2" extension, that script is loaded. If
   not found, 0 is returned. If loading or evaling the (existing) file
   fails, non-0 is returned and the error is reported.
*/
static int s2sh_autoload(s2_engine * se){
  int rc = 0;
  char const * scriptName;
  if(!App.enableArg0Autoload || !App.appName || !*App.appName) return 0;
  else if( !(scriptName = getenv("S2SH_INIT_SCRIPT")) ){
    /*
      See if $0.s2 exists...

      FIXME: this doesn't work reliably if $0 is relative, e.g.
      found via $PATH. We need to resolve its path to do this
      properly, but realpath(3) is platform-specific.
     */
    enum { BufSize = 1024 * 3 };
    static char fn[BufSize];
    cwal_size_t slen;
    char const * exe;
    char const * arg0 = App.appName;
    assert(arg0);
    exe = strstr(arg0, ".exe");
    if(!exe) exe = strstr(arg0, ".EXE");
    if(!exe) exe = strstr(arg0, ".Exe");
    slen = cwal_strlen(arg0) - (exe ? 4 : 0);
    rc = sprintf(fn, "%.*s.s2", (int)slen, arg0);
    assert(rc<BufSize-1);
    rc = 0;
    scriptName = fn;
  }
  if(s2_file_is_accessible(scriptName, 0)){
    cwal_value * rv = 0;
    VERBOSE(("Auto-loading script [%s].\n", scriptName));
    rc = s2_eval_filename(se, 1, scriptName, -1, &rv);
    if(rc){
      MESSAGE(("Error auto-loading init script [%s]: "
               "error #%d (%s).\n", scriptName,
               rc, s2_rc_cstr(rc)));
      s2sh_report_result(se, rc, &rv);
    }
  }
  return rc;
}

int s2sh_main(int argc, char const * const * argv){
  enum {
  UseStackCwalEngine = 1 /* cwal_engine */,
  UseStackS2Engine = 1 /* s2_engine */
  };
  cwal_engine E = cwal_engine_empty;
  cwal_engine * e = UseStackCwalEngine ? &E : 0;
  cwal_engine_vtab vtab = cwal_engine_vtab_basic;
  s2_engine SE = s2_engine_empty;
  s2_engine * se = UseStackS2Engine ? &SE : 0;
  int rc;

  setlocale(LC_CTYPE,"") /* supposedly important for the JSON-parsing bits? */;

  vtab.tracer = cwal_engine_tracer_FILE;
  vtab.tracer.state = stdout;
  vtab.hook.on_init = s2sh_init_engine;
  vtab.interning.is_internable = cstr_is_internable;

  if(App.outFile){
    FILE * of = (0==strcmp("-",App.outFile))
      ? stdout
      : fopen(App.outFile,"wb");
    if(!of){
      rc = CWAL_RC_IO;
      MARKER(("Could not open output file [%s].\n", App.outFile));
      goto end;
    }
    else {
      vtab.outputer.state.data = of;
      vtab.outputer.state.finalize = cwal_finalizer_f_fclose;
      vtab.tracer.state = of;
      vtab.tracer.close = NULL
        /* Reminder: we won't want both outputer and tracer
           to close 'of'.
        */;
    }
  }

  vtab.memcap = App.memcap;

  rc = cwal_engine_init( &e, &vtab );
  if(rc){
    cwal_engine_destroy(e);
    goto end;
  }

  se = UseStackS2Engine ? &SE : s2_engine_alloc(e);
  assert(se);
  assert(UseStackS2Engine ? !se->allocStamp : (se->allocStamp == e));

  if((rc = s2_engine_init( se, e ))) goto end;
  if(!App.cleanroom){
    if((rc = s2_install_core_prototypes(se))) goto end;
  }
  se->sweepInterval += App.addSweepInterval;
  se->vacuumInterval += App.addVacuumInterval;
  se->flags.traceStack = App.traceStacks;
  se->flags.traceAssertions = App.traceAssertions;
  se->flags.traceSweeps = App.traceSweeps;
  s2_clampdown_level_set(se, App.clampDown);

  if( (rc = s2_setup_s2_global(se)) ) goto end;
  s2_set_interrupt_handlable( se )
    /* after infrastructure, before the auto-loaded script, in case it
       contains an endless loop or some such.
    */;
#if defined(S2_SHELL_EXTEND)
  if( (rc = s2_shell_extend(se, argc, argv)) ) goto end;
#endif
  if( (rc=s2sh_autoload(se)) ) goto end;

  rc = s2sh_main2(se);
  App.s2Global = 0;

  end:
  if(App.showMetrics){
    s2_dump_metrics(se);
  }
#if 0
  if(rc && se && se->e && se->err.code){
    char const * msg = 0;
    int serc = 0;
    serc = s2_engine_err_get(se, &msg, 0);
    MESSAGE(("rc=%d (%s): Engine says error #%d (%s)"
             " @ line %d, column %d: %s\n",
             rc, s2_rc_cstr(rc), serc, s2_rc_cstr(serc),
             se->err.line, se->err.col, msg));
  }
#endif
  assert(!se->st.vals.size);
  assert(!se->st.ops.size);
  s2_engine_finalize(se);
  return rc;
}

static void s2sh_show_help(){

  char const * splitter = "\n----------------------------------------";
  printf("s2 script language shell\nUsage: %s [flags] [file]\n", App.appName);

  puts(splitter);
  puts("App-level options:\n");


  puts("\n-?, -help, or --help shows this help and "
       "exits the app successfully.");
  puts("\n-f FILENAME\tRuns the given script file. Reads stdin by default.");
  puts("\n-o FILENAME\tSets the engine's default output channel (some "
       "debug output may go to stdout/stderr). Uses stdout by default.");
  puts("\nThe FILENAME '-' is treated an alias for stdin or stdout.");
  puts("\n-z\tDumps the sizeof() info for the s2-related types to stdout.");
  puts("\n-v\tIncrease verbosity level by one.");
  puts("\n-m\tOutput cwal-/s2-level memory metrics when finished. Use with -v for more info.");
  puts("\n-i\tEnter interactive mode. This is the default when stdin "
       "is a terminal and -f is not specified. If -f FILE is "
       "specified with this option, that file is read first.");
  printf("\n-a, --a\tEnables/disables autoloading of init script named "
         "the same as the app (minus any '.exe' part) plus a '.s2' "
         "extension. Currently %s.\n",
         App.enableArg0Autoload ? "on" : "off");
  printf("\n-cleanroom\tDo not install any global/prototype-level "
         "functionality or process the autoload script. Currently %s.\n",
         App.cleanroom ? "on" : "off");
  puts("\n-- ...\tAll input after -- is ignored by the shell but made available to "
       "script code via the s2.ARGV object.");

  puts("\nMemory capping options:\n");
  puts("\t-cap-ta N: caps the [t]otal cumulative number of [a]llocations at N.\n");
  puts("\t-cap-tb N: caps the [t]otal cumulative number of [b]ytes at N.\n");
  puts("\t-cap-ca N: caps the [c]oncurrent number of [a]llocations at N.\n");
  puts("\t-cap-cb N: caps the [c]oncurrent number of [b]ytes at N.\n");
  puts("\t-cap-sb N: caps any given [s]ingle allocation at N [b]ytes.\n");
  puts("The allocator starts failing (returning NULL) when a capping constraint is violated."
       "\nThe 'tb' and 'ta' constraint violations are permanent (recover requires resetting the engine)."
       "\nThe 'ca' and 'cb' constraints are recoverable once some cwal-managed memory gets freed.\n");

  puts(splitter);
  puts("s2-level options:");

  puts("\n-A\tIncrease assertion trace verbosity level by one.");
  puts("\n-T\tIncrease stack machine trace verbosity level by one.");
  puts("\n--T\tExplicitly disable stack machine trace.");
  puts("\n-W\tIncrease engine sweeping trace verbosity level by one.");
  puts("\n--W\tExplicitly disable engine sweep tracing.");
  printf("\n-w\tIncrease sweep interval by one (currently %d).\n",
         s2_engine_empty.sweepInterval + App.addSweepInterval);
  printf("\n+w\tIncrease vacuum interval by one (currently %d).\n",
         s2_engine_empty.vacuumInterval + App.addVacuumInterval);
  printf("\n+clampdown\tIncreases \"safe mode\" level by one "
         "(disabes some i/o APIs) (currently %d).\n",
         App.clampDown);

  puts(splitter);
  puts("cwal-level options:");

  printf("\n-R|--R\tEnable/disable Value recycling (currently %s).\n",
         App.enableValueRecycling ? "on" : "off");
  printf("\n-C|--C\tEnable/disable memory chunk recycling (currently %s).\n",
         App.maxChunkCount ? "on" : "off");
  printf("\n-S|--S\tEnable/disable string interning (currently %s).\n",
         App.enableStringInterning ? "on" : "off");

  puts("\n");
}

#if 0
static void s2sh_config_memcap(){

  if(0) return /* disable capping (and the related benefits thereof) */;

  /*
    Optional memory/allocation capping support...

    Minor achtung: some capping options require that the allocator
    over-allocate all blocks by sizeof(void*) so that it can store
    their sizes. This does not increase the alloc count, but _does_
    increase memory usage marginally (when recycling is on) or more
    (when recycling is off) and that overhead _does_ count towards
    any capped memory limits. Concretely: when such an option is
    enabled (as noted below), every new allocation will be increased
    by sizeof(void*) bytes so that cwal can record, track, and honor
    the memory caps.  The new bytes are opaque to the client, stored
    in the space before the bytes returned by cwal_malloc() and
    cwal_realloc().
  */

  /* TODO: add CLI options to configure these. */

  App.memcap.maxSingleAllocSize = 1024 * 1024 * 2
    /* allocator will fail any single (re)allocation over
       this size. */
    ;
  App.memcap.maxConcurrentAllocCount = 0
    /* Max number of memory chunks the allocator should serve
       at a time. After this, it will fail until a chunk
       is freed. */
    ;
  App.memcap.maxTotalAllocCount = 0
    /* Caps the cumulative total number of memory allocations made
       by the engine. 0=disabled. OOM errors after this total is hit
       are not recoverable without resetting the engine. */
    ;

  /* The following options require over-allocation: */

  App.memcap.maxConcurrentMem = 1024 * 1024 * 10
    /* allocator will fail if concurrent memory use would exceed
       this. 0=disabled. Requires over-allocation so that realloc
       and free operations can count properly. Reminder to self:
       libfossil/f-s2sh uses ~10-12MB in some tests, but most of it
       is outside of cwal's visibility (fossil buffers and sqlite).
    */
    ;
  App.memcap.maxTotalMem = 0
    /* Caps the cumulative total memory allocated by the engine.
       0=disabled. Requires over-allocation so that reallocs can be
       properly tracked. OOM errors after this total is hit are not
       recoverable without resetting the engine. */;
}
#endif


static int check_flag_val( char const * arg, char const * val ){
  if(!val){
    MARKER(("Missing value for %s flag.\n", arg));
    return CWAL_RC_MISUSE;
  }
  return 0;
}

static cwal_size_t parse_cap_value(char const * arg, int * rc){
  cwal_int_t i = 0;
  if(!s2_cstr_parse_int(arg, -1, &i)){
    MARKER(("-cap-%s must have a positive integer value.\n", arg));
    *rc = CWAL_RC_MISUSE;
    return 0;
  }else if(i<0){
    MARKER(("-cap-%s must have a positive value (got %d).\n", arg, (int)i));
    *rc = CWAL_RC_RANGE;
    return 0;
  }else{
    *rc = 0;
    return (cwal_size_t)i;
  }
}

static int parse_cap_flag( char const * arg, char const * val ){
  int rc;
  char const * pos = arg + 5 /*"-cap-"*/;
  if( (rc = check_flag_val(arg, val)) ) return rc;
  if(2 != strlen(pos)){
    MARKER(("Unknown memory cap flag: %s\n", arg));
    return CWAL_RC_MISUSE;
  }

  if(0==strcmp("ta",pos)){
    App.memcap.maxTotalAllocCount = parse_cap_value(val, &rc)
      /* Caps the cumulative total number of memory allocations made
         by the engine. 0=disabled. OOM errors after this total is hit
         are not recoverable without resetting the engine. */
      ;
  }else if(0==strcmp("tb",pos)){
    App.memcap.maxTotalMem =  parse_cap_value(val, &rc)
      /* Caps the cumulative total memory allocated by the engine.
         0=disabled. Requires over-allocation so that reallocs can be
         properly tracked. OOM errors after this total is hit are not
         recoverable without resetting the engine. */
      ;
  }else if(0==strcmp("ca",pos)){
    App.memcap.maxConcurrentAllocCount = parse_cap_value(val, &rc)
      /* Max number of memory chunks the allocator should serve
         at a time. After this, it will fail until a chunk
         is freed. */
      ;
  }else if(0==strcmp("sb",pos)){
    App.memcap.maxSingleAllocSize = parse_cap_value(val, &rc)
      /* allocator will fail any single (re)allocation over
         this size. */
      ;
  }else if(0==strcmp("cb",pos)){
    App.memcap.maxConcurrentMem = parse_cap_value(val, &rc)
      /* allocator will fail if concurrent memory use would exceed
         this. 0=disabled. Requires over-allocation so that realloc
         and free operations can count properly. Reminder to self:
         libfossil/f-s2sh uses ~10-12MB in some tests, but most of it
         is outside of cwal's visibility (fossil buffers and sqlite).
      */
      ;
  }else{
    MARKER(("Unknown memory cap flag: %s\n", arg));
    rc = CWAL_RC_MISUSE;
  }
  return rc;
}

int main(int argc, char const * const * argv)
{
  int rc = 0;
  int i;
  char const * arg;
  int gotNoOp = 0;
  assert(!App.shellExitFlag);
  App.appName = argv[0];

#if 0
  /* just for one test... */
  App.memcap.maxTotalMem = 1 << 30;
#endif

  for( i = 1; i < argc; ++i ){
    cwal_size_t argLen;
    arg = argv[i];
    argLen = cwal_strlen(arg);
#define ARG(STR) else if(0==strcmp(STR, arg))
#define GET_FLAG_VAL(TGT) \
    if( (rc=check_flag_val(arg, (i<argc-1) ? argv[i+1] : 0)) ) break; \
    else TGT = argv[++i]

    if(('-' != *arg) &&
       ('+' != *arg) &&
       !App.inFile){
      App.inFile = argv[i];
    }ARG("--"){
      /* Signals end of app-level args. Further args
         are for the script world.
      */
      App.scriptArgv = argv+i+1;
      App.scriptArgc = argc-i-1;
      break;
    }ARG("-m"){
      App.showMetrics = 1;
    }ARG("-v"){
      ++App.verboseMode;
    }ARG("-R"){
      App.enableValueRecycling = 1;
    }ARG("--R"){
      App.enableValueRecycling = 0;
    }ARG("-A"){
      ++App.traceAssertions;
    }ARG("-T"){
      ++App.traceStacks;
    }ARG("--T"){
      App.traceStacks = 0;
    }ARG("-W"){
      ++App.traceSweeps;
    }ARG("--W"){
      App.traceSweeps = 0;
    }ARG("-w"){
      ++App.addSweepInterval;
    }ARG("+w"){
      ++App.addVacuumInterval;
    }ARG("-S"){
      App.enableStringInterning = 1;
    }ARG("--S"){
      App.enableStringInterning = 0;
    }ARG("-i"){
      App.interactive = 1;
    }ARG("-f"){
      GET_FLAG_VAL(App.inFile);
      assert(App.inFile);
    }ARG("-o"){
      GET_FLAG_VAL(App.outFile);
    }ARG("-a"){
      App.enableArg0Autoload = 1;
    }ARG("--a"){
      App.enableArg0Autoload = 0;
    }ARG("-C"){
      App.maxChunkCount = 30;
    }ARG("--C"){
      App.maxChunkCount = 0;
    }ARG("-help"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }ARG("--help"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }ARG("-?"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }ARG("-cleanroom"){
      App.cleanroom = 1;
      App.enableArg0Autoload = 0;
    }ARG("+clampdown"){
      ++App.clampDown;
    }else if(argLen>5
             && 0==cwal_compare_cstr("-cap-", 5, arg, 5)){
      char const * val;
      GET_FLAG_VAL(val);
      rc = parse_cap_flag(arg, val);
      if(rc) break;
    }ARG("-z"){
      ++gotNoOp;
#define SO(T) MARKER(("sizeof("#T")=%d\n", (int)sizeof(T)))
      SO(s2_stoken);
      SO(s2_ptoken);
      SO(s2_ptoker);
      SO(s2_op);
      SO(s2_engine);
      SO(cwal_engine);
#undef SO
    }else{
      MARKER(("Unknown argument: %s\n", arg));
      assert(!"Unknown argument");
      return CWAL_RC_MISUSE;
    }
  }
#undef ARG
#undef GET_FLAG_VAL

  if(!rc && !gotNoOp){
    if(!App.inFile && App.interactive<0){
#ifdef S2_OS_UNIX
      if(isatty(STDIN_FILENO)){
        App.interactive = 1;
      }else{
        App.inFile = "-";
      }
#else
      App.inFile = "-";
#endif
    }
    rc = s2sh_main(argc, argv);
  }
  if(rc){
    MARKER(("rc=%d (%s)\n",
            rc, s2_rc_cstr(rc)));
  }
  return rc ? EXIT_FAILURE : EXIT_SUCCESS;
}