diff --git a/include/cubescript/cubescript/ident.hh b/include/cubescript/cubescript/ident.hh index 445ac49..1c31fa5 100644 --- a/include/cubescript/cubescript/ident.hh +++ b/include/cubescript/cubescript/ident.hh @@ -46,6 +46,11 @@ struct command; * * You can also check the actual type with it (cubescript::ident_type) and * decide to cast it to its appropriate specific type, or use the helpers. + * + * An ident always has a valid name. A valid name is pretty much any + * valid Cubescript word (see cubescript::parse_word()) which does not + * begin with a number (a digit, a `+` or `-` followed by a digit or a + * period followed by a digit, or a period followed by a digit). */ struct LIBCUBESCRIPT_EXPORT ident { /** @brief Get the cubescript::ident_type of this ident. */ diff --git a/include/cubescript/cubescript/state.hh b/include/cubescript/cubescript/state.hh index 94e47fa..496bb4a 100644 --- a/include/cubescript/cubescript/state.hh +++ b/include/cubescript/cubescript/state.hh @@ -23,61 +23,292 @@ namespace cubescript { struct state; -using alloc_func = void *(*)(void *, void *, size_t, size_t); +/** @brief The allocator function signature + * + * This is the signature of the function pointer passed to do allocations. + * + * The first argument is the user data, followed by the old pointer (which + * is `nullptr` for new allocations and a valid pointer for reallocations + * and frees). Then follows the original size of the object (zero for new + * allocations, a valid value for reallocations and frees) and the new + * size of the object (zero for frees, a valid value for reallocations + * and new allocations). + * + * It must return the new pointer (`nullptr` when freeing) and does not have + * to throw (the library will throw `std::bad_alloc` itself if it receives + * a `nullptr` upon allocation). + * + * A typical allocation function will look like this: + * + * ``` + * void *my_alloc(void *, void *p, std::size_t, std::size_t ns) { + * if (!ns) { + * std::free(p); + * return nullptr; + * } + * return std::realloc(p, ns); + * } + * ``` + */ +using alloc_func = void *(*)(void *, void *, size_t, size_t); -using hook_func = internal::callable; +/** @brief A call hook function + * + * It is possible to set up a call hook for each thread, which is called + * upon entering the VM. The hook returns nothing and receives the thread + * reference. + */ +using hook_func = internal::callable; + +/** @brief A command function + * + * This is how every command looks. It returns nothing and takes the thread + * reference, a span of input arguments, and a reference to return value. + */ using command_func = internal::callable< void, state &, span_type, any_value & >; +/** @brief The loop state + * + * This is returned by state::run_loop(). + */ enum class loop_state { - NORMAL = 0, BREAK, CONTINUE + NORMAL = 0, /**< @brief The iteration ended normally. */ + BREAK, /**< @brief The iteration was broken out of. */ + CONTINUE /**< @brief The iteration ended early. */ }; +/** @brief The Cubescript thread + * + * Represents a Cubescript thread, either the main thread or a side thread + * depending on how it's created. The state is what you create first and + * also what you should always destroy last. + */ struct LIBCUBESCRIPT_EXPORT state { + /** @brief Create a new Cubescript main thread + * + * This creates a main thread without specifying an allocation function, + * using a simple, builtin implementation. Otherwise it is the same. + */ state(); - state(alloc_func func, void *data); + + /** @brief Create a new Cubescript main thread + * + * For this variant you have to specify a function used to allocate memory. + * The optional data will be passed to allocation every time and is your + * only way to pass custom data to it, since unlike other kinds of hooks, + * the allocation function is a plain function pointer to ensure it never + * allocates by itself. + */ + state(alloc_func func, void *data = nullptr); + + /** @brief Destroy the thread + * + * If the thread is a main thread, all state is destroyed. That means + * main threads should always be destroyed last. + */ virtual ~state(); + /** @brief Cubescript threads are not copyable */ state(state const &) = delete; + + /** @brief Move-construct the Cubescript thread + * + * Keep in mind that you should never use `s` after this is done. + */ state(state &&s); + /** @brief Cubescript threads are not copy assignable */ state &operator=(state const &) = delete; + + /** @brief Move-assign the Cubescript thread + * + * Keep in mind that you should never use `s` after this is done. + * The original `this` is destroyed in the process. + */ state &operator=(state &&s); + /** @brief Swap two Cubescript threads */ void swap(state &s); + /** @brief Create a non-main thread + * + * This creates a non-main thread. You can also create non-main threads + * using other non-main threads, but they will always all be dependent + * on the main thread they originally came from. + * + * @return the thread + */ state new_thread(); + /** @brief Attach a call hook to the thread + * + * The call hook is called every time the VM is entered. You can use + * this for debugging and other tracking, or say, as a means of + * interrupting execution from the side in an interactive interpreter. + */ template hook_func set_call_hook(F &&f) { return set_call_hook( hook_func{std::forward(f), callable_alloc, this} ); } + + /** @brief Get a reference to the call hook */ hook_func const &get_call_hook() const; + + /** @brief Get a reference to the call hook */ hook_func &get_call_hook(); + /** @brief Clear override state for the given ident + * + * If the ident is overridden, clear the flag. Global variables will have + * their value restored to the original, and the changed hook will be + * triggered. Aliases will be set to an empty string. + * + * Other ident types will do nothing. + */ void clear_override(ident &id); + + /** @brief Clear override state for all idents. + * + * @see clear_override() + */ void clear_overrides(); + /** @brief Create a new integer var + * + * @param n the name + * @param v the default value + * @throw cubescript::error in case of redefinition or invalid name + */ integer_var &new_var( std::string_view n, integer_type v, bool read_only = false, var_type vtp = var_type::DEFAULT ); + + /** @brief Create a new float var + * + * @param n the name + * @param v the default value + * @throw cubescript::error in case of redefinition or invalid name + */ float_var &new_var( std::string_view n, float_type v, bool read_only = false, var_type vtp = var_type::DEFAULT ); + + /** @brief Create a new string var + * + * @param n the name + * @param v the default value + * @throw cubescript::error in case of redefinition or invalid name + */ string_var &new_var( std::string_view n, std::string_view v, bool read_only = false, var_type vtp = var_type::DEFAULT ); + + /** @brief Create a new ident + * + * If such ident already exists, nothing will be done and a reference + * will be returned. Otherwise, a new alias will be created and this + * alias will be returned, however it will not be visible from the + * language until actually assigned (it does not exist to the language + * just as is). + * + * @param n the name + * @throw cubescript::error in case of invalid name + */ ident &new_ident(std::string_view n); + /** @brief Reset a variable or alias + * + * This is like clear_override() except it works by name and performs + * extra checks. + * + * @throw cubescript::error if non-existent or read only + */ void reset_var(std::string_view name); + + /** @brief Touch a variable + * + * If an ident with the given name exists and is a global variable, + * a changed hook will be triggered with it, acting like if a new + * value was set, but without actually setting it. + */ void touch_var(std::string_view name); + /** @brief Register a command + * + * This registers a builtin command. A command consists of a valid name, + * a valid argument list, and a function to call. + * + * The argument list is a simple list of types. Currently the following + * simple types are recognized: + * + * * `s` - a string + * * `i` - an integer, default value 0 + * * `b` - an integer, default value `limits::min` + * * `f` - a float, default value 0 + * * `F` - a float, default value is the preceeding value + * * `t` - any (passed as is) + * * `e` - bytecode + * * `E` - condition (see below) + * * `r` - ident + * * `N` - number of real arguments passed up until now + * * `$` - self ident (the command, except for special hooks) + * + * Commands also support variadics. Variadic commands have their type + * list suffixed with `V` or `C`. A `V` variadic is a traditional variadic + * function, while `C` will concatenate all inputs into a single big + * string. + * + * If either `C` or `V` is used alone, the inputs are any arbitrary + * values. However, they can also be used with repetition. Repetition + * works for example like `if2V`. The `2` is the number of types to + * repeat; it must be at most the number of simple types preceeding + * it. It must be followed by `V` or `C`. This specific example means + * that the variadic arguments are a sequence of integer, float, integer, + * float, integer, float and so on. + * + * The resulting command stores the number of arguments it takes. The + * variadic part is not a part of it (neither is the part subject to + * repetition), while all simple types are a part of it (including + * 'fake' ones like argument count). + * + * It is also possible to register special commands. Special commands work + * like normal ones but are special-purpose. The currently allowed special + * commands are `//ivar`, `//fvar`, `//svar` and `//var_changed`. These + * are the only commands where the name can be in this format. + * + * The first three are handlers for for global variables, used when either + * printing or setting them using syntax `varname optional_vals` or using + * `varname = value`. Their type signature must always start with `$` + * and can be followed by any user types, generally you will also want + * to terminate the list with `N` to find out whether any values were + * passed. + * + * This way you can have custom handlers for printing as well as custom + * syntaxes for setting (e.g. your custom integer var handler may want to + * take up to 4 values to allow setting of RGBA color channels). When no + * arguments are passed (checked using `N`) you will want to print the + * value using a format you want. When using the `=` assignment syntax, + * one value is passed. + * + * There are builtin default handlers that take at most one arg (`i`, `f` + * and `s`) which also print to standard output (`name = value`). + * + * For `//var_changed`, there is no default handler. The arg list must be + * just `$`. This will be called whenever a value of an integer, float + * or string builtin variable changes. + * + * For these builtins, `$` will refer to the variable ident, not to the + * builtin command. + * + * @throw cubescript::error upon redefinition, invalid name or arg list + */ template command &new_command( std::string_view name, std::string_view args, F &&f @@ -88,30 +319,149 @@ struct LIBCUBESCRIPT_EXPORT state { ); } + /** @brief Get a specific cubescript::ident (or `nullptr`) */ ident *get_ident(std::string_view name); + + /** @brief Get a specific cubescript::alias (or `nullptr`) */ alias *get_alias(std::string_view name); + + /** @brief Check if a cubescript::ident of the given name exists */ bool have_ident(std::string_view name); + /** @brief Get a span of all idents */ span_type get_idents(); + + /** @brief Get a span of all idents */ span_type get_idents() const; + /** @brief Execute the given bytecode reference + * + * @return the return value + */ any_value run(bcode_ref const &code); + + /** @brief Execute the given string as code + * + * @return the return value + */ any_value run(std::string_view code); + + /** @brief Execute the given string as code + * + * This variant takes a file name to be included in debug information. + * While the library provides no way to deal with file I/O, this is a + * support function to make implementing these better. + * + * @param source a source file name + * + * @return the return value + */ any_value run(std::string_view code, std::string_view source); + + /** @brief Execute the given ident + * + * If a command, it will simply be executed with the given arguments, + * ensuring that missing ones are filled in and types are set properly. + * If a builtin variable, the appropriate handler will be called. If + * an alias, the value of it will be compiled and executed. Any other + * ident type will simply do nothing. + * + * @return the return value + */ any_value run(ident &id, span_type args); + /** @brief Execute a loop body + * + * This exists to implement custom loop commands. A loop command will + * consist of your desired loop and will take a body as an argument + * (with bytecode type); this body will be run using this API. The + * return value can be used to check if the loop was broken out of + * or continued, and take steps accordingly. + * + * Some loops may evaluate to values, while others may not. + */ loop_state run_loop(bcode_ref const &code, any_value &ret); + + /** @brief Execute a loop body + * + * This version ignores the return value of the body. + */ loop_state run_loop(bcode_ref const &code); + /** @brief Get if the thread is in override mode + * + * If the thread is in override mode, any assigned alias or variable will + * be given the overridden flag, with variables also saving their old + * value. Upon clearing the flag (using clear_override() or similar) + * the old value will be restored (aliases will be set to an empty + * string). + * + * Overridable variables will always act like if the thread is in override + * mode, even if it's not. + * + * Keep in mind that if an alias is pushed, its flags will be cleared once + * popped. + * + * @see set_override_mode() + */ bool get_override_mode() const; + + /** @brief Set the thread's override mode + * + * @see get_override_mode() + */ bool set_override_mode(bool v); + /** @brief Get if the thread is in persist most + * + * In persist mode, newly assigned aliases will have the persist flag + * set on them, which is an indicator that they should be saved to disk + * like persistent variables. The library does no saving, so by default + * it works as an indicator for the user. + * + * Keep in mind that if an alias is pushed, its flags will be cleared once + * popped. + * + * @see set_persist_mode() + */ bool get_persist_mode() const; + + /** @brief Set the thread's persist mode + * + * @see get_persist_mode() + */ bool set_persist_mode(bool v); + /** @brief Get the maximum run depth of the VM + * + * If zero, it is unlimited, otherwise it specifies how much the VM is + * allowed to recurse. By default, it is zero. + * + * @see set_max_run_depth() + */ std::size_t get_max_run_depth() const; + + /** @brief Set the maximum run depth ov the VM + * + * If zero, it is unlimited (this is the default). You can limit how much + * the VM is allowed to recurse if you have specific constraints to adhere + * to. + * + * @return the old value + */ std::size_t set_max_run_depth(std::size_t v); + /** @brief Set a variable + * + * This will set something of the given name to the given value. The + * something may be a variable or an alias. + * + * If no ident of such name exists, a new alias will be created and + * set. + * + * @throw cubescript::error if `name` is a builtin ident (a registered + * command or similar) or if it is invalid + */ void set_alias(std::string_view name, any_value v); private: @@ -136,11 +486,78 @@ private: struct thread_state *p_tstate = nullptr; }; +/** @brief Initialize the base library + * + * You can choose which parts of the standard library you include in your + * program. The base library contains core constructs for things such as + * error handling, conditionals, looping, and var/alias management. + * + * Calling this multiple times has no effect; commands will only be + * registered once. + * + * @see cubescript::std_init_math() + * @see cubescript::std_init_string() + * @see cubescript::std_init_list() + * @see cubescript::std_init_all() + */ LIBCUBESCRIPT_EXPORT void std_init_base(state &cs); + +/** @brief Initialize the math library + * + * You can choose which parts of the standard library you include in your + * program. The math library contains arithmetic and other math related + * functions. + * + * Calling this multiple times has no effect; commands will only be + * registered once. + * + * @see cubescript::std_init_base() + * @see cubescript::std_init_string() + * @see cubescript::std_init_list() + * @see cubescript::std_init_all() + */ LIBCUBESCRIPT_EXPORT void std_init_math(state &cs); + +/** @brief Initialize the string library + * + * You can choose which parts of the standard library you include in your + * program. The string library contains commands to manipulate strings. + * + * Calling this multiple times has no effect; commands will only be + * registered once. + * + * @see cubescript::std_init_base() + * @see cubescript::std_init_math() + * @see cubescript::std_init_list() + * @see cubescript::std_init_all() + */ LIBCUBESCRIPT_EXPORT void std_init_string(state &cs); + +/** @brief Initialize the list library + * + * You can choose which parts of the standard library you include in your + * program. The list library contains commands to manipulate lists. + * + * Calling this multiple times has no effect; commands will only be + * registered once. + * + * @see cubescript::std_init_base() + * @see cubescript::std_init_math() + * @see cubescript::std_init_string() + * @see cubescript::std_init_all() + */ LIBCUBESCRIPT_EXPORT void std_init_list(state &cs); +/** @brief Initialize all standard libraries + * + * This is like calling each of the individual standard library init + * functions and exists mostly just for convenience. + + * @see cubescript::std_init_base() + * @see cubescript::std_init_math() + * @see cubescript::std_init_string() + * @see cubescript::std_init_list() + */ LIBCUBESCRIPT_EXPORT void std_init_all(state &cs); } /* namespace cubescript */ diff --git a/include/cubescript/cubescript/util.hh b/include/cubescript/cubescript/util.hh index fc95b31..9327d5e 100644 --- a/include/cubescript/cubescript/util.hh +++ b/include/cubescript/cubescript/util.hh @@ -20,21 +20,80 @@ namespace cubescript { +/** @brief A safe alias handler for commands + * + * In general, when dealing with aliases in commands, you do not want to + * set them directly, since this would set the alias globally. Instead, you + * can use this to make aliases local to the command. + * + * Internally, each Cubescript thread has a mapping for alias state within + * the thread. This mapping is stack based - which means you can push an + * alias, and then anything affecting the value of the alias in that thread + * will only be visible until the stack is popped. This structure provides + * a safe means of handling the alias stack; constructing it will push the + * alias, destroying it will pop it. + * + * Therefore, what you can do is something like this: + * + * ``` + * if (alias_local s{my_thread, &my_thread.new_ident("test")}; s) { + * // branch taken when the alias was successfully pushed + * // setting the alias will only be visible within this scope + * s.set(some_value); // a convenient setter + * my_thread.run(...); + * } else { + * // you can handle an error here + * } + * ``` + * + * The `else` branch can happen one case; either the given ident is `nullptr` + * (which will never happen here) or it's not a cubescript::alias (which can + * happen if an ident of such name already exists and is not an alias). If + * it fails, obviously no push/pop happens. + * + * Since the goal is to interact tightly with RAII and ensure consistency at + * all times, it is not possible to copy or move this object. That means you + * should also not be storing it; it should be used purely as a scope based + * alias stack manager. + */ struct LIBCUBESCRIPT_EXPORT alias_local { + /** @brief Construct the local handler */ alias_local(state &cs, ident *a); + + /** @brief Destroy the local handler */ ~alias_local(); + /** @brief Local handlers are not copyable */ alias_local(alias_local const &) = delete; + + /** @brief Local handlers are not movable */ alias_local(alias_local &&) = delete; + /** @brief Local handlers are not copy assignable */ alias_local &operator=(alias_local const &) = delete; + + /** @brief Local handlers are not move assignable */ alias_local &operator=(alias_local &&v) = delete; + /** @brief Get the contained alias + * + * @return the alias or `nullptr` if none set + */ alias *get_alias() noexcept { return p_alias; } + + /** @brief Get the contained alias + * + * @return the alias or `nullptr` if none set + */ alias const *get_alias() const noexcept { return p_alias; } + /** @brief Set the contained alias's value + * + * @return `true` if the alias is valid, `false` otherwise + */ bool set(any_value val); + /** @brief Get if there is an alias associated with this handler */ explicit operator bool() const noexcept; private: @@ -42,34 +101,105 @@ private: void *p_sp; }; +/** @brief A list parser + * + * Cubescript does not have data structures and everything is a string. + * However, you can represent lists as strings; there is a standard syntax + * to them. + * + * A list in Cubescript is simply a bunch of items separated by whitespace. + * The items can take the form of any literal value Cubescript has. That means + * they can be number literals, they can be words, and they can be strings. + * Strings can be quoted either with double quotes, square brackets or even + * parenthesis; basically any syntax representing a value. + * + * Comments (anything following two slashes, inclusive) are skipped. As far + * as allowed whitespace consisting an item delimiter goes, this is either + * regular spaces, horizontal tabs, or newlines. + * + * Keep in mind that it does not own the string it is parsing. Therefore, + * you have to make sure to keep it alive for as long as the parser is. + * + * The input string by itself should not be quoted. + */ struct LIBCUBESCRIPT_EXPORT list_parser { + /** @brief Construct a list parser. + * + * Nothing is done until you actually start parsing. + * + * @param cs the thread + * @param s the string representing the list + */ list_parser(state &cs, std::string_view s = std::string_view{}): p_state{&cs}, p_input_beg{s.data()}, p_input_end{s.data() + s.size()} {} + /** @brief Reset the input string for the list */ void set_input(std::string_view s) { p_input_beg = s.data(); p_input_end = s.data() + s.size(); } + /** @brief Get the current input string in the parser + * + * The already read items will not be contained in the result. + */ std::string_view get_input() const { return std::string_view{ p_input_beg, std::size_t(p_input_end - p_input_beg) }; } + /** @brief Attempt to parse an item + * + * This will first skip whitespace and then attempt to read an element. + * + * @return `true` if an element was found, `false` otherwise + */ bool parse(); + + /** @brief Get the number of items in the current list + * + * This will not contain items that are already parsed out, and will + * parse the list itself, i.e. the final state will be an empty list. + */ std::size_t count(); + /** @brief Get the currently parsed item + * + * If the item was quoted with double quotes, the contents will be run + * through cubescript::unescape_string() first. + * + * @see get_raw_item() + * @see get_quoted_item() + */ string_ref get_item() const; + /** @brief Get the currently parsed raw item + * + * Unlike get_item(), this will not unescape the string under any + * circumstances and represents simply a slice of the original input. + * + * @see get_item() + * @see get_quoted_item() + */ std::string_view get_raw_item() const { return std::string_view{p_ibeg, std::size_t(p_iend - p_ibeg)}; } + + /** @brief Get the currently parsed raw item + * + * Like get_raw_item(), but contains the quotes too, if there were any. + * Likewise, the resulting view is just a slice of the original input. + * + * @see get_item() + * @see get_raw_item() + */ std::string_view get_quoted_item() const { return std::string_view{p_qbeg, std::size_t(p_qend - p_qbeg)}; } + /** @brief Skip whitespace in the input until a value is reached. */ void skip_until_item(); private: @@ -80,11 +210,36 @@ private: char const *p_qbeg{}, *p_qend{}; }; - +/** @brief Parse a double quoted Cubescript string + * + * This parses double quoted strings according to the Cubescript syntax. The + * string has to begin with a double quote; if it does not for any reason, + * `str.data()` is returned. + * + * Escape sequences are not expanded and have the syntax `^X` where X is the + * specific escape character (e.g. `^n` for newline). It is possible to make + * the string multiline; the line needs to end with `\\`. + * + * Strings must be terminated again with double quotes. + * + * @param cs the thread + * @param str the input string + * @param[out] nlines the number of lines in the string + * + * @return a pointer to the character after the last double quotes + * @throw cubescript::error if the string is started but not finished + * + * @see cubescript::parse_word() + */ LIBCUBESCRIPT_EXPORT char const *parse_string( state &cs, std::string_view str, size_t &nlines ); +/** @brief Parse a double quoted Cubescript string + * + * This overload has the same semantics but it does not return the number + * of lines. + */ inline char const *parse_string( state &cs, std::string_view str ) { @@ -92,15 +247,50 @@ inline char const *parse_string( return parse_string(cs, str, nlines); } +/** @brief Parse a Cubescript word. + * + * A Cubescript word is a sequence of any characters that are not whitespace + * (spaces, newlines, tabs) or a comment (two consecutive slashes). It is + * allowed to have parenthesis and square brackets as long a they are balanced. + * + * Examples of valid words: `foo`, `test123`, `125.4`, `[foo]`, `hi(bar)`. + * + * If a non-word character is encountered immediately, the resulting pointer + * will be `str.data()`. + * + * Keep in mind that a valid word may not be a valid ident name (e.g. numbers + * are valid words but not valid ident names). + * + * @return a pointer to the first character after the word + * @throw cubescript::error if there is unbalanced `[` or `(` + */ LIBCUBESCRIPT_EXPORT char const *parse_word( state &cs, std::string_view str ); +/** @brief Concatenate a span of values + * + * The input values are concatenated by `sep`. Non-integer/float/string + * input values are considered empty strings. Integers and floats are + * converted to strings. The input list is not affected, however. + */ LIBCUBESCRIPT_EXPORT string_ref concat_values( state &cs, span_type vals, std::string_view sep = std::string_view{} ); +/** @brief Escape a Cubescript string + * + * This reads and input string and writes it into `writer`, treating special + * characters as escape sequences. Newlines are turned into `^n`, tabs are + * turned into `^t`, vertical tabs into `^f`; double quotes are prefixed + * with a caret, carets are duplicated. All other characters are passed + * through. + * + * @return `writer` after writing into it + * + * @see cubescript::unescape_string() + */ template inline R escape_string(R writer, std::string_view str) { *writer++ = '"'; @@ -118,6 +308,21 @@ inline R escape_string(R writer, std::string_view str) { return writer; } +/** @brief Unscape a Cubescript string + * + * If a caret is encountered, it is skipped. If the following character is `n`, + * it is turned into a newline; `t` is turned into a tab, `f` into a vertical + * tab, double quote is written as is, as is a second caret. Any others are + * written as they are. + * + * If a backslash is encountered and followed by a newline, the sequence is + * skipped, otherwise the backslash is written out. Any other character is + * written out as is. + * + * @return `writer` after writing into it + * + * @see cubescript::unescape_string() + */ template inline R unescape_string(R writer, std::string_view str) { for (auto it = str.begin(); it != str.end(); ++it) { @@ -128,7 +333,7 @@ inline R unescape_string(R writer, std::string_view str) { } switch (*it) { case 'n': *writer++ = '\n'; break; - case 't': *writer++ = '\r'; break; + case 't': *writer++ = '\t'; break; case 'f': *writer++ = '\f'; break; case '"': *writer++ = '"'; break; case '^': *writer++ = '^'; break; @@ -156,6 +361,18 @@ inline R unescape_string(R writer, std::string_view str) { return writer; } +/** @brief Print a Cubescript stack + * + * This prints out the Cubescript stack as stored in cubescript::error, into + * the `writer`. Each level is written on its own line. The line starts with + * two spaces. If there is a gap in the stack and we've reached index 1, + * the two spaces are followed with two periods. Following that is the index + * followed by a right parenthesis, a space, and the name of the ident. + * + * The last line is not terminated with a newline. + * + * @return `writer` after writing into it + */ template inline R print_stack(R writer, stack_state const &st) { char buf[32] = {0}; @@ -172,6 +389,7 @@ inline R print_stack(R writer, stack_state const &st) { char const *p = buf; std::copy(p, p + strlen(p), writer); *writer++ = ')'; + *writer++ = ' '; std::copy(name.begin(), name.end(), writer); nd = nd->next; if (nd) {