/* -*- 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;
  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;
} App = {
0/*appName*/,
1/*enableArg0Autoload*/,
1/*enableValueRecycling*/,
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*/
};

/**
   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);

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

      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*/;

    /* Reminder: HASH is not currently recyclable due (ironically) to
       a type-specific memory optimization (namely, that the hash
       table is not resizable). Refactoring to make it
       resizable/recycleable is pending. */

    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.
                       */;
  }
#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_exception_set(se->e, 0);
      assert(*rv);
      cwal_props_sort(*rv);
      break;
    case CWAL_RC_EXIT:
      rc = 0;
      *rv = cwal_propagating_take(se->e);
      break;
    case CWAL_RC_BREAK:
      label = "UNHANDLED BREAK";
      *rv = cwal_propagating_take(se->e);
      break;
    case CWAL_RC_CONTINUE:
      label = "UNHANDLED CONTINUE";
      *rv = 0;
      break;
    case CWAL_RC_FATAL:
      label = "FATAL";
      *rv = cwal_propagating_take(se->e);
      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_value_ref(*rv);
    cwal_value_unref(*rv);
#endif
    *rv = 0;
  }
  cwal_exception_set(se->e, 0);
  cwal_propagating_set(se->e, 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_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);

  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;
  assert(App.s2Global);
  if((rc = s2_install_shell_api(se, App.s2Global, "shell"))){
    return rc;
  }

  s2sh_history_load(App.lnHistoryFile) /* ignore errors */;
  rc = cwal_var_decl( se->e, 0, "g", 1,
                      cwal_new_object_value(se->e), CWAL_VAR_F_CONST );
  if(rc) return rc;
  
  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"
               "The 'g' object is at your service as a place to "
               "store things.\n");

  while( (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;
  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

  /*
    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;

  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;

#define FUNC2(NAME,FP)                              \
    v = s2_new_function2( se, FP ); \
    VCHECK; \
    rc = cwal_prop_set( c, NAME, cwal_strlen(NAME), v );        \
    if(rc) {cwal_value_unref(v); goto end;}(void)0

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

  FUNC2("dumpMetrics", s2_cb_dump_metrics);
  FUNC2("dumpVal", s2_cb_dump_val);
  FUNC2("isCallable", s2_cb_is_callable);
  FUNC2("isDereffable", s2_cb_is_dereffable);
  FUNC2("getenv", s2_cb_getenv);
  FUNC2("import", s2_cb_import_script);
  FUNC2("loadModule", s2_cb_module_load);
  FUNC2("tmpl", s2_cb_tmpl_to_code);
  FUNC2("fork", s2_cb_fork);

  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;
    }
  }


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

static int s2sh_main2( s2_engine * se ){
  int rc = 0;
  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'.
        */;
    }
  }

  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((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;

  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");
  puts("\n-- ...\tAll input after -- is ignored by the shell but made available to "
       "script code via the s2.ARGV object.");


  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);

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

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

  puts("\n");
}

int main(int argc, char const * const * argv)
{
  int rc = 0;
  int i;
  char const * arg;
  int gotNoOp = 0;
  App.appName = argv[0];
  for( i = 1; i < argc; ++i ){
    arg = argv[i];
#define ARG(STR) else if(0==strcmp(STR, arg))
    if(('-' != *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"){
      App.inFile = (i<argc-1)
        ? argv[++i]
        : 0;
      assert(App.inFile);
    }ARG("-o"){
      App.outFile = (i==argc-1) ? "-" : argv[++i];
    }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
    }ARG("-a"){
      App.enableArg0Autoload = 1;
    }ARG("--a"){
      App.enableArg0Autoload = 0;
    }ARG("-help"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }ARG("--help"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }ARG("-?"){
      gotNoOp = 1;
      s2sh_show_help();
      break;
    }else{
      MARKER(("Unknown argument: %s\n", arg));
      assert(!"Unknown argument");
      return CWAL_RC_MISUSE;
    }
  }
#undef ARG

  if(!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;
}
