feat(frontends/lean/parser): add parse_level
Signed-off-by: Leonardo de Moura <leonardo@microsoft.com>
This commit is contained in:
parent
cffbae3667
commit
3bde699fbe
10 changed files with 198 additions and 13 deletions
|
@ -9,6 +9,18 @@ Author: Leonardo de Moura
|
||||||
|
|
||||||
namespace lean {
|
namespace lean {
|
||||||
static name g_raw("raw");
|
static name g_raw("raw");
|
||||||
|
|
||||||
|
environment universe_cmd(parser & p) {
|
||||||
|
if (p.curr_is_identifier()) {
|
||||||
|
// TODO(Leo): take namespace and scopes into account
|
||||||
|
name n = p.get_name_val();
|
||||||
|
p.next();
|
||||||
|
return p.env().add_global_level(n);
|
||||||
|
} else {
|
||||||
|
throw parser_error("invalid universe declaration, identifier expected", p.cmd_pos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
environment print_cmd(parser & p) {
|
environment print_cmd(parser & p) {
|
||||||
if (p.curr() == scanner::token_kind::String) {
|
if (p.curr() == scanner::token_kind::String) {
|
||||||
p.regular_stream() << p.get_str_val() << "\n";
|
p.regular_stream() << p.get_str_val() << "\n";
|
||||||
|
@ -25,7 +37,8 @@ environment print_cmd(parser & p) {
|
||||||
|
|
||||||
cmd_table init_cmd_table() {
|
cmd_table init_cmd_table() {
|
||||||
cmd_table r;
|
cmd_table r;
|
||||||
add_cmd(r, cmd_info("print", "print a string", print_cmd));
|
add_cmd(r, cmd_info("print", "print a string", print_cmd));
|
||||||
|
add_cmd(r, cmd_info("universe", "declare a global universe level", universe_cmd));
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,23 @@ Released under Apache 2.0 license as described in the file LICENSE.
|
||||||
Author: Leonardo de Moura
|
Author: Leonardo de Moura
|
||||||
*/
|
*/
|
||||||
#include "frontends/lean/builtin_exprs.h"
|
#include "frontends/lean/builtin_exprs.h"
|
||||||
|
#include "frontends/lean/parser.h"
|
||||||
|
|
||||||
namespace lean {
|
namespace lean {
|
||||||
namespace notation {
|
namespace notation {
|
||||||
|
static name g_llevel_curly(".{");
|
||||||
|
static name g_rcurly("}");
|
||||||
|
|
||||||
|
static expr parse_Type(parser & p, unsigned, expr const *) {
|
||||||
|
if (p.curr_is_token(g_llevel_curly)) {
|
||||||
|
p.next();
|
||||||
|
level l = p.parse_level();
|
||||||
|
p.check_token_next(g_rcurly, "invalid Type expression, '}' expected");
|
||||||
|
return mk_sort(l);
|
||||||
|
} else {
|
||||||
|
return mk_sort(p.mk_new_level_param());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parse_table init_nud_table() {
|
parse_table init_nud_table() {
|
||||||
action Expr(mk_expr_action());
|
action Expr(mk_expr_action());
|
||||||
|
@ -19,6 +33,7 @@ parse_table init_nud_table() {
|
||||||
r.add({transition("(", Expr), transition(")", Skip)}, x0);
|
r.add({transition("(", Expr), transition(")", Skip)}, x0);
|
||||||
r.add({transition("fun", Binders), transition(",", mk_scoped_expr_action(x0))}, x0);
|
r.add({transition("fun", Binders), transition(",", mk_scoped_expr_action(x0))}, x0);
|
||||||
r.add({transition("Pi", Binders), transition(",", mk_scoped_expr_action(x0, 0, false))}, x0);
|
r.add({transition("Pi", Binders), transition(",", mk_scoped_expr_action(x0, 0, false))}, x0);
|
||||||
|
r.add({transition("Type", mk_ext_action(parse_Type))}, x0);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,7 +145,7 @@ action mk_exprs_action(name const & sep, expr const & rec, expr const & ini, boo
|
||||||
action mk_scoped_expr_action(expr const & rec, unsigned rb, bool lambda) {
|
action mk_scoped_expr_action(expr const & rec, unsigned rb, bool lambda) {
|
||||||
return action(new scoped_expr_action_cell(rec, rb, lambda));
|
return action(new scoped_expr_action_cell(rec, rb, lambda));
|
||||||
}
|
}
|
||||||
action mk_ext_parse_action(parse_fn const & fn) { return action(new ext_action_cell(fn)); }
|
action mk_ext_action(parse_fn const & fn) { return action(new ext_action_cell(fn)); }
|
||||||
|
|
||||||
struct parse_table::cell {
|
struct parse_table::cell {
|
||||||
bool m_nud;
|
bool m_nud;
|
||||||
|
|
|
@ -61,7 +61,7 @@ public:
|
||||||
friend action mk_binder_action();
|
friend action mk_binder_action();
|
||||||
friend action mk_binders_action();
|
friend action mk_binders_action();
|
||||||
friend action mk_scoped_expr_action(expr const & rec, unsigned rbp, bool lambda);
|
friend action mk_scoped_expr_action(expr const & rec, unsigned rbp, bool lambda);
|
||||||
friend action mk_ext_parse_action(parse_fn const & fn);
|
friend action mk_ext_action(parse_fn const & fn);
|
||||||
|
|
||||||
action_kind kind() const;
|
action_kind kind() const;
|
||||||
unsigned rbp() const;
|
unsigned rbp() const;
|
||||||
|
@ -81,7 +81,7 @@ action mk_exprs_action(name const & sep, expr const & rec, expr const & ini, boo
|
||||||
action mk_binder_action();
|
action mk_binder_action();
|
||||||
action mk_binders_action();
|
action mk_binders_action();
|
||||||
action mk_scoped_expr_action(expr const & rec, unsigned rbp = 0, bool lambda = true);
|
action mk_scoped_expr_action(expr const & rec, unsigned rbp = 0, bool lambda = true);
|
||||||
action mk_proc_action(parse_fn const & fn);
|
action mk_ext_action(parse_fn const & fn);
|
||||||
|
|
||||||
class transition {
|
class transition {
|
||||||
name m_token;
|
name m_token;
|
||||||
|
|
|
@ -174,6 +174,15 @@ bool parser::curr_is_token(name const & tk) const {
|
||||||
get_token_info().value() == tk;
|
get_token_info().value() == tk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool parser::curr_is_token_or_id(name const & tk) const {
|
||||||
|
if (curr() == scanner::token_kind::Keyword || curr() == scanner::token_kind::CommandKeyword)
|
||||||
|
return get_token_info().value() == tk;
|
||||||
|
else if (curr() == scanner::token_kind::Identifier)
|
||||||
|
return get_name_val() == tk;
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void parser::check_token_next(name const & tk, char const * msg) {
|
void parser::check_token_next(name const & tk, char const * msg) {
|
||||||
if (!curr_is_token(tk))
|
if (!curr_is_token(tk))
|
||||||
throw parser_error(msg, pos());
|
throw parser_error(msg, pos());
|
||||||
|
@ -209,6 +218,124 @@ static name g_lcurly("{");
|
||||||
static name g_rcurly("}");
|
static name g_rcurly("}");
|
||||||
static name g_lbracket("[");
|
static name g_lbracket("[");
|
||||||
static name g_rbracket("]");
|
static name g_rbracket("]");
|
||||||
|
static name g_add("+");
|
||||||
|
static name g_max("max");
|
||||||
|
static name g_imax("imax");
|
||||||
|
static name g_cup("\u2294");
|
||||||
|
static unsigned g_level_add_prec = 10;
|
||||||
|
static unsigned g_level_cup_prec = 5;
|
||||||
|
|
||||||
|
unsigned parser::parse_small_nat() {
|
||||||
|
auto p = pos();
|
||||||
|
mpz val = get_num_val().get_numerator();
|
||||||
|
next();
|
||||||
|
lean_assert(val >= 0);
|
||||||
|
if (!val.is_unsigned_int())
|
||||||
|
throw parser_error("invalid level expression, value does not fit in a machine integer", p);
|
||||||
|
return val.get_unsigned_int();
|
||||||
|
}
|
||||||
|
|
||||||
|
static level lift(level l, unsigned k) {
|
||||||
|
while (k > 0) {
|
||||||
|
k--;
|
||||||
|
l = mk_succ(l);
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned parser::curr_level_lbp() const {
|
||||||
|
if (curr_is_token(g_cup))
|
||||||
|
return g_level_cup_prec;
|
||||||
|
else if (curr_is_token(g_add))
|
||||||
|
return g_level_add_prec;
|
||||||
|
else
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::parse_max_imax(bool is_max) {
|
||||||
|
auto p = pos();
|
||||||
|
next();
|
||||||
|
buffer<level> lvls;
|
||||||
|
while (curr_is_identifier() || curr_is_numeral() || curr_is_token(g_lparen)) {
|
||||||
|
lvls.push_back(parse_level());
|
||||||
|
}
|
||||||
|
if (lvls.size() < 2)
|
||||||
|
throw parser_error("invalid level expression, max must have at least two arguments", p);
|
||||||
|
unsigned i = lvls.size() - 1;
|
||||||
|
level r = lvls[i];
|
||||||
|
while (i > 0) {
|
||||||
|
--i;
|
||||||
|
if (is_max)
|
||||||
|
r = mk_max(lvls[i], r);
|
||||||
|
else
|
||||||
|
r = mk_imax(lvls[i], r);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::parse_level_id() {
|
||||||
|
auto p = pos();
|
||||||
|
name id = get_name_val();
|
||||||
|
next();
|
||||||
|
auto it = m_local_level_decls.find(id);
|
||||||
|
if (it != m_local_level_decls.end())
|
||||||
|
return it->second.first;
|
||||||
|
if (m_env.is_global_level(id))
|
||||||
|
return mk_global_univ(id);
|
||||||
|
throw parser_error(sstream() << "unknown level '" << id << "'", p);
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::parse_level_nud() {
|
||||||
|
if (curr_is_token_or_id(g_max)) {
|
||||||
|
return parse_max_imax(true);
|
||||||
|
} else if (curr_is_token_or_id(g_imax)) {
|
||||||
|
return parse_max_imax(false);
|
||||||
|
} else if (curr_is_token(g_lparen)) {
|
||||||
|
next();
|
||||||
|
level l = parse_level();
|
||||||
|
check_token_next(g_rparen, "invalid level expression, ')' expected");
|
||||||
|
return l;
|
||||||
|
} else if (curr_is_numeral()) {
|
||||||
|
unsigned k = parse_small_nat();
|
||||||
|
return lift(level(), k);
|
||||||
|
} else if (curr_is_identifier()) {
|
||||||
|
return parse_level_id();
|
||||||
|
} else {
|
||||||
|
throw parser_error("invalid level expression", pos());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::parse_level_led(level left) {
|
||||||
|
auto p = pos();
|
||||||
|
if (curr_is_token(g_cup)) {
|
||||||
|
next();
|
||||||
|
level right = parse_level(g_level_cup_prec);
|
||||||
|
return mk_max(left, right);
|
||||||
|
} else if (curr_is_token(g_add)) {
|
||||||
|
next();
|
||||||
|
if (curr_is_numeral()) {
|
||||||
|
unsigned k = parse_small_nat();
|
||||||
|
return lift(left, k);
|
||||||
|
} else {
|
||||||
|
throw parser_error("invalid level expression, right hand side of '+' (aka universe lift operator) must be a numeral", p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw parser_error("invalid level expression", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::parse_level(unsigned rbp) {
|
||||||
|
level left = parse_level_nud();
|
||||||
|
while (rbp < curr_level_lbp()) {
|
||||||
|
left = parse_level_led(left);
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
level parser::mk_new_level_param() {
|
||||||
|
// TODO(Leo)
|
||||||
|
return level();
|
||||||
|
}
|
||||||
|
|
||||||
/** \brief Parse <tt>ID ':' expr</tt>, where the expression represents the type of the identifier. */
|
/** \brief Parse <tt>ID ':' expr</tt>, where the expression represents the type of the identifier. */
|
||||||
parameter parser::parse_binder_core(binder_info const & bi) {
|
parameter parser::parse_binder_core(binder_info const & bi) {
|
||||||
|
|
|
@ -44,7 +44,9 @@ struct interrupt_parser {};
|
||||||
|
|
||||||
class parser {
|
class parser {
|
||||||
typedef std::pair<expr, unsigned> local_entry;
|
typedef std::pair<expr, unsigned> local_entry;
|
||||||
|
typedef std::pair<level, unsigned> local_level_entry;
|
||||||
typedef scoped_map<name, local_entry, name_hash, name_eq> local_decls;
|
typedef scoped_map<name, local_entry, name_hash, name_eq> local_decls;
|
||||||
|
typedef scoped_map<name, local_level_entry, name_hash, name_eq> local_level_decls;
|
||||||
|
|
||||||
environment m_env;
|
environment m_env;
|
||||||
io_state m_ios;
|
io_state m_ios;
|
||||||
|
@ -56,6 +58,7 @@ class parser {
|
||||||
scanner m_scanner;
|
scanner m_scanner;
|
||||||
scanner::token_kind m_curr;
|
scanner::token_kind m_curr;
|
||||||
local_decls m_local_decls;
|
local_decls m_local_decls;
|
||||||
|
local_level_decls m_local_level_decls;
|
||||||
pos_info m_last_cmd_pos;
|
pos_info m_last_cmd_pos;
|
||||||
pos_info m_last_script_pos;
|
pos_info m_last_script_pos;
|
||||||
unsigned m_next_tag_idx;
|
unsigned m_next_tag_idx;
|
||||||
|
@ -94,10 +97,18 @@ class parser {
|
||||||
cmd_table const & cmds() const { return cfg().m_cmds; }
|
cmd_table const & cmds() const { return cfg().m_cmds; }
|
||||||
parse_table const & nud() const { return cfg().m_nud; }
|
parse_table const & nud() const { return cfg().m_nud; }
|
||||||
parse_table const & led() const { return cfg().m_led; }
|
parse_table const & led() const { return cfg().m_led; }
|
||||||
|
/** \brief Return true if the current token is a keyword named \c tk or an identifier named \c tk */
|
||||||
|
bool curr_is_token_or_id(name const & tk) const;
|
||||||
|
|
||||||
void add_local_entry(name const & n, expr const & e);
|
void add_local_entry(name const & n, expr const & e);
|
||||||
void add_local_decl(expr const & t);
|
void add_local_decl(expr const & t);
|
||||||
|
|
||||||
|
unsigned curr_level_lbp() const;
|
||||||
|
level parse_max_imax(bool is_max);
|
||||||
|
level parse_level_id();
|
||||||
|
level parse_level_nud();
|
||||||
|
level parse_level_led(level left);
|
||||||
|
|
||||||
void parse_command();
|
void parse_command();
|
||||||
void parse_script(bool as_expr = false);
|
void parse_script(bool as_expr = false);
|
||||||
bool parse_commands();
|
bool parse_commands();
|
||||||
|
@ -126,6 +137,10 @@ public:
|
||||||
script_state * ss() const { return m_ss; }
|
script_state * ss() const { return m_ss; }
|
||||||
|
|
||||||
void parse_names(buffer<std::pair<pos_info, name>> & result);
|
void parse_names(buffer<std::pair<pos_info, name>> & result);
|
||||||
|
unsigned parse_small_nat();
|
||||||
|
|
||||||
|
level parse_level(unsigned rbp = 0);
|
||||||
|
level mk_new_level_param();
|
||||||
|
|
||||||
parameter parse_binder();
|
parameter parse_binder();
|
||||||
void parse_binders(buffer<parameter> & r);
|
void parse_binders(buffer<parameter> & r);
|
||||||
|
@ -145,6 +160,7 @@ public:
|
||||||
expr rec_save_pos(expr const & e, pos_info p);
|
expr rec_save_pos(expr const & e, pos_info p);
|
||||||
pos_info pos_of(expr const & e, pos_info default_pos);
|
pos_info pos_of(expr const & e, pos_info default_pos);
|
||||||
pos_info pos_of(expr const & e) { return pos_of(e, pos()); }
|
pos_info pos_of(expr const & e) { return pos_of(e, pos()); }
|
||||||
|
pos_info cmd_pos() const { return m_last_cmd_pos; }
|
||||||
|
|
||||||
/** \brief Read the next token. */
|
/** \brief Read the next token. */
|
||||||
void scan() { m_curr = m_scanner.scan(m_env); }
|
void scan() { m_curr = m_scanner.scan(m_env); }
|
||||||
|
@ -152,9 +168,11 @@ public:
|
||||||
scanner::token_kind curr() const { return m_curr; }
|
scanner::token_kind curr() const { return m_curr; }
|
||||||
/** \brief Return true iff the current token is an identifier */
|
/** \brief Return true iff the current token is an identifier */
|
||||||
bool curr_is_identifier() const { return curr() == scanner::token_kind::Identifier; }
|
bool curr_is_identifier() const { return curr() == scanner::token_kind::Identifier; }
|
||||||
|
/** \brief Return true iff the current token is a numeral */
|
||||||
|
bool curr_is_numeral() const { return curr() == scanner::token_kind::Numeral; }
|
||||||
/** \brief Read the next token if the current one is not End-of-file. */
|
/** \brief Read the next token if the current one is not End-of-file. */
|
||||||
void next() { if (m_curr != scanner::token_kind::Eof) scan(); }
|
void next() { if (m_curr != scanner::token_kind::Eof) scan(); }
|
||||||
/** \brief Return true iff the current token is equal to \c tk */
|
/** \brief Return true iff the current token is a keyword (or command keyword) named \c tk */
|
||||||
bool curr_is_token(name const & tk) const;
|
bool curr_is_token(name const & tk) const;
|
||||||
/** \brief Check current token, and move to next characther, throw exception if current token is not \c tk. */
|
/** \brief Check current token, and move to next characther, throw exception if current token is not \c tk. */
|
||||||
void check_token_next(name const & tk, char const * msg);
|
void check_token_next(name const & tk, char const * msg);
|
||||||
|
|
|
@ -39,16 +39,19 @@ static char const * g_lambda_unicode = "\u03BB";
|
||||||
static char const * g_pi_unicode = "\u03A0";
|
static char const * g_pi_unicode = "\u03A0";
|
||||||
static char const * g_forall_unicode = "\u2200";
|
static char const * g_forall_unicode = "\u2200";
|
||||||
static char const * g_arrow_unicode = "\u2192";
|
static char const * g_arrow_unicode = "\u2192";
|
||||||
|
static char const * g_cup = "\u2294";
|
||||||
|
|
||||||
|
|
||||||
token_table init_token_table() {
|
token_table init_token_table() {
|
||||||
token_table t;
|
token_table t;
|
||||||
char const * builtin[] = {"fun", "Pi", "let", "in", "have", "show", "by", "from", "(", ")", "{", "}", "[", "]",
|
char const * builtin[] = {"fun", "Pi", "let", "in", "have", "show", "by", "from", "(", ")", "{", "}", "[", "]",
|
||||||
".{", "Type", "...", ",", ".", ":", "calc", ":=", "--", "(*", "(--", "->",
|
".{", "Type", "...", ",", ".", ":", "calc", ":=", "--", "(*", "(--", "->",
|
||||||
"proof", "qed", "private", "raw", "Bool", nullptr};
|
"proof", "qed", "private", "raw", "Bool", "+", g_cup, nullptr};
|
||||||
|
|
||||||
char const * commands[] = {"theorem", "axiom", "variable", "definition", "evaluate", "check",
|
char const * commands[] = {"theorem", "axiom", "variable", "definition", "evaluate", "check",
|
||||||
"print", "variables", "end", "namespace", "section", "import",
|
"print", "variables", "end", "namespace", "section", "import",
|
||||||
"abbreviation", "inductive", "record", "structure", "module", nullptr};
|
"abbreviation", "inductive", "record", "structure", "module", "universe",
|
||||||
|
nullptr};
|
||||||
|
|
||||||
std::pair<char const *, char const *> aliases[] =
|
std::pair<char const *, char const *> aliases[] =
|
||||||
{{g_lambda_unicode, "fun"}, {"forall", "Pi"}, {g_forall_unicode, "Pi"}, {g_pi_unicode, "Pi"},
|
{{g_lambda_unicode, "fun"}, {"forall", "Pi"}, {g_forall_unicode, "Pi"}, {g_pi_unicode, "Pi"},
|
||||||
|
|
|
@ -147,12 +147,12 @@ static void tst2() {
|
||||||
// check_name("x.10", name(name("x"), 10));
|
// check_name("x.10", name(name("x"), 10));
|
||||||
check_name("x.bla", name({"x", "bla"}));
|
check_name("x.bla", name({"x", "bla"}));
|
||||||
|
|
||||||
scan_error("+++");
|
scan_error("***");
|
||||||
environment env;
|
environment env;
|
||||||
token_table s = mk_default_token_table();
|
token_table s = mk_default_token_table();
|
||||||
s = add_token(s, "+++", "tplus");
|
s = add_token(s, "***", "tplus");
|
||||||
env = update_token_table(env, s);
|
env = update_token_table(env, s);
|
||||||
check_keyword("+++", "tplus", env);
|
check_keyword("***", "tplus", env);
|
||||||
s = add_token(s, "+", "plus");
|
s = add_token(s, "+", "plus");
|
||||||
env = update_token_table(env, s);
|
env = update_token_table(env, s);
|
||||||
check("x+y", {tk::Identifier, tk::Keyword, tk::Identifier}, env);
|
check("x+y", {tk::Identifier, tk::Keyword, tk::Identifier}, env);
|
||||||
|
@ -188,7 +188,7 @@ static void tst3() {
|
||||||
scan_error("\"\\");
|
scan_error("\"\\");
|
||||||
scan_error("\"\\a");
|
scan_error("\"\\a");
|
||||||
scan("\"\naaa\"");
|
scan("\"\naaa\"");
|
||||||
scan_error("foo.+ 01");
|
scan_error("foo.* 01");
|
||||||
scan("10.0.");
|
scan("10.0.");
|
||||||
scan("{ } . forall exists let in \u2200 := _");
|
scan("{ } . forall exists let in \u2200 := _");
|
||||||
}
|
}
|
||||||
|
|
9
tests/lean/run/t2.lean
Normal file
9
tests/lean/run/t2.lean
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
universe l
|
||||||
|
universe u
|
||||||
|
print raw Type
|
||||||
|
print raw Type.{1}
|
||||||
|
print raw Type.{2}
|
||||||
|
print raw Type.{l+1}
|
||||||
|
print raw Type.{max l u 1}
|
||||||
|
print raw Type.{imax l+1 u 1}
|
||||||
|
print raw Type.{imax l+1 l u}
|
|
@ -22,8 +22,8 @@ s = s:add_command_token("tast", "tst2")
|
||||||
s = s:add_command_token("tests", "tst3")
|
s = s:add_command_token("tests", "tst3")
|
||||||
s = s:add_command_token("fests", "tst4")
|
s = s:add_command_token("fests", "tst4")
|
||||||
s = s:add_command_token("tes", "tst5")
|
s = s:add_command_token("tes", "tst5")
|
||||||
s = s:add_token("++", "++", 65)
|
s = s:add_token("**", "**", 65)
|
||||||
s = s:add_token("++-", "plusminus")
|
s = s:add_token("**-", "ttimeminus")
|
||||||
assert(token_table_size(s) == 7)
|
assert(token_table_size(s) == 7)
|
||||||
display_token_table(s)
|
display_token_table(s)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue