/* -*- 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 #if defined(S2_AMALGAMATION_BUILD) # include "s2_amalgamation.h" #else # include "s2.h" /* #include "s2_t10n.h" */ #endif #if defined(S2_OS_UNIX) #include /* fork() and friends */ #include /* pid_t, if not picked up via unistd.h */ #include /* errno! */ #endif #include #include #include #include /* 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 /* 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(rcallocStamp : (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