#include #include "pprint.hpp" #include "util.hpp" #include "table.hpp" #include class PrintMachine { public: LuaVar tabchpos_; LuaExtStack LS_; int next_id_; bool indent_; std::ostream *output_; // When a general table appears in the output, we use the character // position where the table first appeared as a unique ID for the // general table. eng::map chpos_to_table_number_; eng::map chpos_to_header_; void atomic_print(int xtype, LuaSlot val, bool quote) { switch (xtype) { case LUA_TNIL: { (*output_) << "nil"; return; } case LUA_TSTRING: { if (quote) { util::quote_string(LS_.ckstring(val), output_); } else { // TODO: this could be more efficient. (*output_) << LS_.ckstring(val); } return; } case LUA_TNUMBER: { double value = LS_.cknumber(val); if (std::isnan(value)) { (*output_) << "nan"; } else { int64_t ivalue = int64_t(value); if (double(ivalue) == value) { (*output_) << ivalue; } else { (*output_) << value; } } return; } case LUA_TBOOLEAN: { (*output_) << (LS_.ckboolean(val) ? "true" : "false"); return; } case LUA_TFUNCTION: { (*output_) << ""; return; } case LUA_TTHREAD: { (*output_) << ""; return; } case LUA_TLIGHTUSERDATA: { LuaToken token = LS_.cktoken(val); (*output_) << "[" << token.str() << "]"; return; } case LUA_TT_GENERAL: { eng::string classname = LS_.classname(val); if (classname.empty()) { (*output_) << ""; } else { (*output_) << "<" << classname << ">"; } return; } case LUA_TT_TANGIBLE: { eng::string classname = LS_.classname(val); if (classname.empty()) { (*output_) << ""; } else { (*output_) << ""; } return; } case LUA_TT_CLASS: { (*output_) << ""; return; } case LUA_TT_GLOBALENV: { (*output_) << ""; return; } case LUA_TT_TANGIBLEMETA: { (*output_) << ""; return; } default: { (*output_) << ""; return; }} } void tabify(int level) { if (indent_) { (*output_) << std::endl; for (int i = 0; i < level; i++) { (*output_) << " "; } } else { (*output_) << " "; } } void pprint_r(int level, bool expand, LuaSlot value) { lua_State *L = LS_.state(); lua_checkstack(L, 20); LuaVar loffset, pairs, key, val, lchpos; LuaExtStack LS(L, loffset, pairs, key, val, lchpos); // Determine the extended type of the object. If the // expand flag is true, try to coerce it to a general table. int xtype = LS.xtype(value); eng::string classname = LS.classname(value); // Print the atomic portion or table header. // Make a decision about whether to print key-value pairs, // if not, then return. if (xtype < LUA_TT_GENERAL) { atomic_print(xtype, value, true); return; } else if (xtype > LUA_TT_GENERAL) { atomic_print(xtype, value, true); if (!expand) return; } else { LS.rawget(lchpos, tabchpos_, value); int chpos = LS.tryint(lchpos).value_or(-1); if (chpos < 0) { // First time. // * The character position where the table first appears // serves as a unique ID for the table. Record it in the tabchpos // map. Then, generate an initial tentative header for the table, // and insert it into the header map. Do not output the header to // output stream: it will be post-injected into the output stream. chpos = int((*output_).tellp()); LS.rawset(tabchpos_, value, chpos); if (!classname.empty()) { chpos_to_header_[chpos] = util::ss("<", classname, ">"); } } else { // Second time. // See if the table already has a table number assigned. If not, // assign it one. Update the header for the table. // Do output the header to the output stream, it is only // post-injected at the first table appearance. int tabnum = chpos_to_table_number_[chpos]; if (tabnum == 0) { tabnum = next_id_++; chpos_to_table_number_[chpos] = tabnum; if (classname.empty()) { chpos_to_header_[chpos] = util::ss("
"); } else { chpos_to_header_[chpos] = util::ss("<", classname, " ", tabnum, ">"); } } (*output_) << chpos_to_header_[chpos]; return; // Do not output key-value pairs the second time. } } // How many keys in the table? int nkeys = LS.nkeys(value); // Decide whether we're going to print a line for the metatable. // If the table has a classname, we don't need, to, because the // classname (which is in the header) tell the reader what metatable // is being used. Also, tangible metatables are not shown, because // tangibles keep secret stuff in the metatable. (this may change). bool print_meta = false; if (classname.empty()) { LS.getmetatable(val, value); if (LS.istable(val) && (LS.gettabletype(val) != LUA_TT_TANGIBLEMETA)) { print_meta = true; } } // If it's an array of atomic values, without a visible metatable, // we're going to print it without newlines. Scan the table to see if // it's possible to print it array-style. bool array_style = false; if (!print_meta) { array_style = true; for (int i = 1; i <= nkeys; i++) { LS.rawget(val, value, i); if (LS.isnil(val) || (LS.type(val) == LUA_TTABLE)) { array_style = false; break; } } } // Print it array-style. This code is simple because // we don't do indentation, and we don't handle any values // that aren't atomic. if (array_style) { (*output_) << "{"; for (int i = 1; i <= nkeys; i++) { if (i > 1) (*output_) << ", "; LS.rawget(val, value, i); atomic_print(LS.xtype(val), val, true); } (*output_) << "}"; } // Print it table-style. if (!array_style) { table_getpairs(LS, value, pairs, true); (*output_) << "{"; bool needcomma = false; for (int i = 2; ; i+=2) { LS.rawget(key, pairs, i); if (LS.isnil(key)) break; LS.rawget(val, pairs, i+1); if (needcomma) (*output_) << ","; needcomma = true; tabify(level + 1); if (LS.isstring(key) && sv::is_lua_id(LS.ckstring(key))) { (*output_) << LS.ckstring(key); } else if (LS.istoken(key)) { atomic_print(LUA_TLIGHTUSERDATA, key, false); } else { (*output_) << "["; pprint_r(level + 1, false, key); (*output_) << "]"; } if (indent_) { (*output_) << " = "; } else { (*output_) << "="; } pprint_r(level + 1, false, val); } if (print_meta) { LS.getmetatable(val, value); if (needcomma) (*output_) << ","; tabify(level + 1); (*output_) << " = "; pprint_r(level + 1, false, val); } tabify(level); (*output_) << "}"; } } // Atomic print interface. PrintMachine(LuaCoreStack &LS0, LuaSlot root, bool quote, std::ostream *os) : LS_(LS0.state(), tabchpos_) { output_ = os; atomic_print(LS_.xtype(root), root, quote); } // Pretty print interface. PrintMachine(LuaCoreStack &LS0, LuaSlot root, bool indent, int level, bool expand, std::ostream *os) : LS_(LS0.state(), tabchpos_) { next_id_ = 1; indent_ = indent; LS_.newtable(tabchpos_); util::ostringstream preoutput; output_ = &preoutput; pprint_r(level, expand, root); std::string_view pre = preoutput.view(); // Output the results. We would just copy the characters // one by one to the target stream, except that we have to // insert table headers wherever the chpos_to_header map says // so. chpos_to_header_.emplace(0x7FFFFFFF, ""); auto iter = chpos_to_header_.begin(); for (int i = 0; i < int(pre.size()); i++) { if (i == iter->first) { (*os) << iter->second; iter++; } (*os).put(pre[i]); } } }; void PrettyPrintOptions::parse(LuaKeywordParser &kp) { LuaVar option; LuaExtStack LS(kp.state(), option); kp.check_throw(); if (kp.optional(option, "indent")) { indent = LS.ckboolean(option); } if (kp.optional(option, "level")) { level = LS.ckint(option); } if (kp.optional(option, "expand")) { expand = LS.ckboolean(option); } } void atomic_print(LuaCoreStack &LS, LuaSlot val, bool quote, std::ostream *os) { PrintMachine pm(LS, val, quote, os); } void pprint(LuaCoreStack &LS, LuaSlot val, const PrettyPrintOptions &opts, std::ostream *os) { PrintMachine pm(LS, val, opts.indent, opts.level, opts.expand, os); } LuaDefine(string_pprint, "obj1, obj2, ...", "|Pretty-print the specified objects into a string." "|" "|See also: string.pprintx, which has a lot more options." "|This function uses the default options: pretty print indented," "|start at indentation level zero, and always expand the" "|top-level table." "|") { LuaRet result; LuaExtraArgs extra; LuaDefStack LS(L, result, extra); util::ostringstream oss; for (int i = 0; i < extra.size(); i++) { pprint(LS, extra[i], PrettyPrintOptions(), &oss); oss << "\n"; } LS.set(result, oss.view()); return LS.result(); } LuaDefine(string_pprintx, "options", "|Pretty-print the specified object into a string, with options" "|" "|Options is a table with these fields:" "|" "| value - the object to pretty-print" "| indent - if false, suppress newlines and indentation (default: true)" "| level - base level of indentation (default: zero)" "| expand - if true, force expansion of top-level table (default: false)" "|" "|About the expand flag: normally, when you print a class, it just " "|prints '', and when you print a tangible, it just" "|prints ''. But sometimes, you want to see the details." "|The expand flag forces it to expand the top-level table, even if the" "|top-level table is a tangible or class." "|") { LuaArg loptions; LuaRet result; LuaVar value; LuaDefStack LS(L, loptions, result, value); PrettyPrintOptions options; LuaKeywordParser kp(LS, loptions); options.parse(kp); if (!kp.optional(value, "value")) { LS.set(value, LuaNil); } kp.final_check_throw(); util::ostringstream oss; pprint(LS, value, options, &oss); LS.set(result, oss.view()); return LS.result(); } LuaDefine(string_print, "obj", "|Concise print the specified object into a string" "|" "|This prints a concise representation of obj into a string. Tables" "|are not expanded: for that, use string.pprint or string.pprintx." "|" "|The functions string.print and tostring are identical." "|") { LuaArg val; LuaRet result; LuaDefStack LS(L, val, result); eng::ostringstream oss; atomic_print(LS, val, false, &oss); LS.set(result, oss.str()); return LS.result(); } LuaDefineAlias(tostring, string_print); LuaDefine(string_isidentifier, "str", "return true if the string is a valid lua identifier") { LuaArg str; LuaRet result; LuaDefStack LS(L, str, result); if (LS.isstring(str)) { eng::string s = LS.ckstring(str); LS.set(result, sv::is_lua_id(s)); } else { LS.set(result, false); } return LS.result(); }