function kw(word) { let pattern = Array.from(word).reduce( (acc, letter) => acc + `[${letter}${letter.toUpperCase()}]`, "" ); return new RegExp(pattern); } function separated(separator, rule) { return optional(separated1(separator, rule)); } function separated1(separator, rule) { return seq(rule, repeat(seq(separator, rule))); } function commaSep1(rule) { return separated1(",", rule); } function commaSep(rule) { return optional(commaSep1(rule)); } module.exports = grammar({ name: "plpgsql", // NOTE(chrde): https://github.com/tree-sitter/tree-sitter-javascript/blob/1ddbf1588c353edab37791cdcc9f17e56fb4ea73/grammar.js#L9 extras: ($) => [$.comment, /[\s\uFEFF\u2060\u200B\u00A0]/], rules: { source_file: ($) => repeat(choice($.psql_statement, seq($._statement, ";"))), _statement: ($) => choice( $.psql_statement, $.create_function_statement, $.drop_function_statement, $.drop_type_statement, $.create_table_statement, $.create_schema_statement, $.create_type_statement, $.select_statement, $.insert_statement, $.delete_statement, $.update_statement, $.grant_statement, $.create_trigger_statement, $.create_sequence_statement, $.create_index_statement, $.alter_table_statement, $.do_block ), drop_type_statement: ($) => seq( kw("drop"), kw("type"), optional($.if_exists), commaSep1($.identifier), optional(choice(kw("cascade"), kw("restrict"))) ), update_statement: ($) => seq( optional($.with_query), kw("update"), $.identifier, optional(seq(optional(kw("as")), $.identifier)), kw("set"), commaSep1($.update_set), optional(seq(kw("from"), commaSep1($.from_item))), optional($.where_filter), optional($.returning) ), drop_function_statement: ($) => seq( kw("drop"), kw("function"), commaSep1($.drop_function_item), optional(choice(kw("cascade"), kw("restrict"))) ), drop_function_item: ($) => seq( optional($.if_exists), $.identifier, optional(seq("(", commaSep(choice($.var_declaration, $._type)), ")")) ), create_type_statement: ($) => seq( kw("create"), kw("type"), $.identifier, optional( choice( seq(kw("as"), kw("enum"), "(", commaSep1($.string), ")"), seq(kw("as"), "(", commaSep1($.var_declaration), ")") ) ) ), // TODO(chrde): values _with_query_statement: ($) => choice( $.select_statement, $.insert_statement, $.delete_statement, $.update_statement ), insert_statement: ($) => seq( optional($.with_query), kw("insert"), kw("into"), $.identifier, optional($.as), optional(alias($._list_of_identifiers, $.columns)), $.insert_items, optional($.insert_conflict), optional($.returning), optional($.into) ), insert_items: ($) => choice( seq(kw("default"), kw("values")), seq(kw("values"), commaSep1($.insert_values)), $.select_statement, seq("(", $.select_statement, ")") ), insert_values: ($) => seq("(", commaSep($.insert_item), ")"), insert_item: ($) => choice(kw("default"), $._value_expression), insert_conflict: ($) => choice( seq( kw("on"), kw("conflict"), optional($.conflict_target), kw("do"), kw("nothing") ), seq( kw("on"), kw("conflict"), $.conflict_target, kw("do"), kw("update"), kw("set"), commaSep1($.update_set), optional($.where_filter) ) ), conflict_target: ($) => choice( seq(kw("on"), kw("constraint"), $.identifier), seq("(", commaSep($._value_expression), ")") ), update_set: ($) => choice( seq($.identifier, "=", $.update_value), seq( $._list_of_identifiers, "=", optional(kw("row")), "(", commaSep1($.update_value), ")" ) ), update_value: ($) => choice(kw("default"), $._value_expression), returning: ($) => seq(kw("returning"), commaSep1($.select_item)), create_table_statement: ($) => seq( kw("create"), optional($.temporary), optional($.unlogged), kw("table"), optional($.if_not_exists), $.identifier, "(", commaSep($.create_table_item), ")" ), create_table_item: ($) => choice($.table_column_item, $.table_constraint), create_schema_statement: ($) => seq( kw("create"), kw("schema"), optional($.if_not_exists), choice(seq($.identifier, optional($.schema_role)), $.schema_role) ), schema_role: ($) => seq( kw("authorization"), choice($.identifier, kw("current_user"), kw("session_user")) ), create_index_statement: ($) => seq( kw("create"), optional(kw("unique")), kw("index"), optional(kw("concurrently")), optional($.if_not_exists), optional($.identifier), kw("on"), $.identifier, optional($.index_using), "(", commaSep1($.index_col), ")", optional($.index_includes), optional($.where_filter) ), index_using: ($) => seq(kw("using"), $.identifier), index_col: ($) => choice( seq( $.identifier, optional($.index_col_dir), optional($.index_col_nulls) ), seq( "(", $._value_expression, ")", optional($.index_col_dir), optional($.index_col_nulls) ) ), index_col_dir: ($) => choice(kw("asc"), kw("desc")), index_col_nulls: ($) => choice(seq(kw("nulls"), kw("first")), seq(kw("nulls"), kw("last"))), index_includes: ($) => seq(kw("include"), $._list_of_identifiers), delete_statement: ($) => seq( optional($.with_query), kw("delete"), kw("from"), $.identifier, optional(kw("as")), optional($.identifier), optional($.delete_using), optional($.where_filter), optional(seq(kw("returning"), commaSep1($.select_item))), optional($.into) ), delete_using: ($) => seq(kw("using"), commaSep1($.from_item)), alter_table_statement: ($) => seq( kw("alter"), kw("table"), optional($.if_exists), $.identifier, $.alter_table_change ), alter_table_change: ($) => choice( commaSep1($.alter_table_action), $.alter_table_rename_column, $.alter_table_rename_constraint, $.alter_table_rename_table, $.alter_table_change_schema ), alter_table_action: ($) => choice( seq(kw("add"), $.table_constraint), seq( kw("add"), optional(kw("column")), optional($.if_not_exists), $.table_column_item ), seq( kw("drop"), kw("constraint"), optional($.if_exists), $.identifier, optional($.alter_table_fk_ref_action) ), seq( kw("drop"), optional(kw("column")), optional($.if_exists), $.identifier, optional($.alter_table_fk_ref_action) ), seq( kw("alter"), optional(kw("column")), $.identifier, optional($.alter_column_action) ) ), alter_column_action: ($) => choice( seq(kw("set"), kw("default"), $._value_expression), seq(kw("drop"), kw("default")), seq(kw("set"), kw("not"), kw("null")), seq(kw("drop"), kw("not"), kw("null")), seq(kw("type"), $.alter_column_type), seq(kw("set"), kw("data"), kw("type"), $.alter_column_type) ), table_constraint: ($) => choice( seq($.table_constraint_ty, optional($.constraint_when)), seq( kw("constraint"), $.identifier, $.table_constraint_ty, optional($.constraint_when) ) ), constraint_when: ($) => choice( kw("deferrable"), seq(kw("deferrable"), kw("initially"), kw("immediate")), seq(kw("deferrable"), kw("initially"), kw("deferred")) ), table_constraint_ty: ($) => choice( seq(kw("check"), "(", $._value_expression, ")"), seq(kw("unique"), $._list_of_identifiers), seq(kw("primary"), kw("key"), $._list_of_identifiers), seq( kw("foreign"), kw("key"), $._list_of_identifiers, $.constraint_foreign_key ) ), constraint_foreign_key: ($) => seq( kw("references"), $.identifier, optional($._list_of_identifiers), repeat($.fk_action) ), fk_action: ($) => choice( seq(kw("on"), kw("delete"), $.fk_ref_action), seq(kw("on"), kw("update"), $.fk_ref_action) ), fk_ref_action: ($) => choice( seq(kw("no"), kw("action")), kw("restrict"), kw("cascade"), seq(kw("set"), kw("null")), seq(kw("set"), kw("default")) ), alter_column_type: ($) => seq($._type, optional(seq(kw("using"), $._value_expression))), alter_table_fk_ref_action: ($) => choice(kw("restrict"), kw("cascade")), table_column_item: ($) => seq($.identifier, $._type, repeat($.column_constraint)), column_constraint: ($) => choice( seq( kw("constraint"), $.identifier, $.column_constraint_ty, optional($.constraint_when) ), seq($.column_constraint_ty, optional($.constraint_when)) ), column_constraint_ty: ($) => choice( seq(kw("not"), kw("null")), kw("null"), seq(kw("check"), "(", $._value_expression, ")"), seq(kw("default"), $._value_expression), kw("unique"), seq(kw("primary"), kw("key")), $.constraint_foreign_key ), alter_table_rename_column: ($) => seq( kw("rename"), optional(kw("column")), $.identifier, kw("to"), $.identifier ), alter_table_rename_constraint: ($) => seq(kw("rename"), kw("constraint"), $.identifier, kw("to"), $.identifier), alter_table_rename_table: ($) => seq(kw("rename"), kw("to"), $.identifier), alter_table_change_schema: ($) => seq(kw("set"), kw("schema"), $.identifier), grant_statement: ($) => seq( kw("grant"), $.grant_privileges, kw("on"), $.grant_targets, kw("to"), $.grant_roles ), grant_roles: ($) => commaSep1( choice( kw("public"), kw("current_user"), kw("session_user"), seq(optional(kw("group")), $.identifier) ) ), grant_privileges: ($) => choice( seq(kw("all"), optional(kw("privileges"))), commaSep1($.identifier) ), grant_targets: ($) => choice( seq( kw("all"), choice(kw("tables"), kw("sequences"), kw("functions")), kw("in"), kw("schema"), $.identifier ), seq(kw("sequence"), commaSep1($.identifier)), seq(optional(kw("table")), commaSep1($.identifier)), seq(kw("schema"), commaSep1($.identifier)), seq( choice(kw("function"), kw("procedure"), kw("routine")), commaSep1($.grant_function) ) ), grant_function: ($) => seq( $.identifier, "(", commaSep1(seq(optional($.identifier), $._type)), ")" ), grant_all_in_schema: ($) => seq(kw("in"), kw("schema")), psql_statement: ($) => seq("\\", repeat1($.identifier), /[\n\r]/), create_sequence_statement: ($) => seq( kw("create"), optional($.temporary), kw("sequence"), optional($.if_not_exists), $.identifier, repeat( choice( $.as, $.sequence_increment, $.sequence_min, $.sequence_max, $.sequence_start, $.sequence_cache, $.sequence_cycle, $.sequence_owned ) ) ), sequence_increment: ($) => seq(kw("increment"), optional(kw("by")), $.number), sequence_min: ($) => choice(seq(kw("no"), kw("minvalue")), seq(kw("minvalue"), $.number)), sequence_max: ($) => choice(seq(kw("no"), kw("maxvalue")), seq(kw("maxvalue"), $.number)), sequence_start: ($) => seq(kw("start"), optional(kw("with")), $.number), sequence_cache: ($) => seq(kw("cache"), $.number), sequence_cycle: ($) => seq(optional(kw("no")), kw("cycle")), sequence_owned: ($) => seq(kw("owned"), kw("by"), choice(kw("none"), $.identifier)), create_trigger_statement: ($) => seq( kw("create"), optional(kw("constraint")), kw("trigger"), $.identifier, $.trigger_when, $.trigger_event, kw("on"), $.identifier, optional($.trigger_scope), optional($.trigger_cond), $.trigger_exec ), trigger_when: ($) => choice(kw("before"), kw("after"), kw("instead of")), trigger_event: ($) => separated1( kw("or"), choice(kw("insert"), kw("update"), kw("delete"), kw("truncate")) ), trigger_scope: ($) => seq( optional(seq(kw("for"), optional(kw("each")))), choice(kw("statement"), kw("row")) ), trigger_exec: ($) => seq( kw("execute"), optional(choice(kw("procedure"), kw("function"))), $.function_call ), trigger_cond: ($) => seq(kw("when"), "(", $._value_expression, ")"), _plpgsql_statement: ($) => seq( choice( $._statement, $.assign_statement, $.get_diagnostics_statement, $.open_cursor_statement, $.return_statement, $.raise_statement, $.if_statement, $.for_statement, $.execute_statement, $.perform_statement ), ";" ), open_cursor_statement: ($) => seq( kw("open"), $.identifier, kw("for"), choice($.select_statement, $.execute_statement) ), get_diagnostics_statement: ($) => seq( kw("get"), optional(kw("current")), kw("diagnostics"), $.assign_statement ), for_statement: ($) => seq( kw("for"), commaSep1($.identifier), kw("in"), choice( seq( optional(kw("reverse")), $._value_expression, "..", $._value_expression, optional(seq(kw("by"), $._value_expression)) ), $.select_statement, $.execute_statement ), kw("loop"), repeat($._plpgsql_statement), kw("end"), kw("loop") ), // TODO(chrde): https://www.postgresql.org/docs/13/plpgsql-errors-and-messages.html raise_statement: ($) => seq( kw("raise"), optional( seq( optional($.identifier), $.string, optional(seq(",", commaSep($._value_expression))) ) ) ), if_statement: ($) => seq( kw("if"), $._value_expression, kw("then"), repeat1($._plpgsql_statement), repeat( seq( choice(kw("elsif"), kw("elseif")), $._value_expression, kw("then"), repeat1($._plpgsql_statement) ) ), optional(seq(kw("else"), repeat1($._plpgsql_statement))), kw("end"), kw("if") ), execute_statement: ($) => seq( kw("execute"), $._value_expression, optional($.into), optional($.execute_using) ), execute_using: ($) => seq(kw("using"), commaSep1($._value_expression)), assign_statement: ($) => seq($.identifier, choice("=", ":="), $._value_expression), return_statement: ($) => seq( kw("return"), choice( seq(kw("query"), $.select_statement), seq(kw("query"), $.execute_statement), $._value_expression ) ), perform_statement: ($) => seq(kw("perform"), commaSep($.select_item)), do_block: ($) => seq(kw("do"), $.block), select_statement: ($) => prec.left( seq( optional($.with_query), kw("select"), commaSep($.select_item), optional($.into), optional($.select_from), optional($.select_where), optional($.select_group_by), optional($.select_having), optional($.select_order_by), optional($._select_limit_offset), optional($.into) ) ), with_query: ($) => seq(kw("with"), commaSep1($.with_query_item)), with_query_item: ($) => seq( $.identifier, optional($._list_of_identifiers), kw("as"), optional( choice(kw("materialized"), seq(kw("not"), kw("materialized"))) ), "(", $._with_query_statement, ")" ), into: ($) => seq(kw("into"), optional(kw("strict")), commaSep1($.identifier)), select_having: ($) => seq(kw("having"), $._value_expression), _select_limit_offset: ($) => choice( seq($.select_limit, optional($.select_offset)), seq($.select_offset, optional($.select_limit)) ), select_limit: ($) => seq(kw("limit"), choice(kw("all"), $._value_expression)), select_offset: ($) => seq( kw("offset"), $._value_expression, optional(choice(kw("row"), kw("rows"))) ), // TODO(chrde): rollup, cube, grouping sets select_group_by: ($) => prec( 1, seq( kw("group"), kw("by"), choice( seq("(", commaSep1($._value_expression), ")"), commaSep1($._value_expression) ) ) ), select_order_by: ($) => seq(kw("order"), kw("by"), commaSep1($.order_by_item)), order_by_item: ($) => seq($._value_expression, optional($.order_by_direction)), order_by_direction: ($) => choice(kw("asc"), kw("desc")), select_where: ($) => $.where_filter, select_item: ($) => seq($._value_expression, optional(kw("as")), optional($.identifier)), select_from: ($) => seq(kw("from"), commaSep1($.from_item)), from_item: ($) => prec.left( seq( // TODO(chrde): https://www.postgresql.org/docs/current/sql-select.html choice($.from_select, $.from_table, $.from_function), repeat($.join_item) ) ), from_select: ($) => seq("(", $.select_statement, ")", optional(kw("as")), $.identifier), from_table: ($) => seq($.identifier, optional(kw("as")), optional($.identifier)), from_function: ($) => seq( $.function_call, optional( choice( seq(kw("as"), $.identifier, optional($._list_of_identifiers)), seq($.identifier, optional($._list_of_identifiers)), seq(kw("as"), $._list_of_identifiers) ) ) ), join_item: ($) => prec.left( choice( seq(kw("natural"), $.join_type, $.from_item), seq($.join_type, $.from_item, $.join_condition), seq(kw("cross"), kw("join"), $.from_item) ) ), join_condition: ($) => choice( seq(kw("on"), $._value_expression), seq(kw("using"), $._list_of_identifiers) ), join_type: ($) => seq( choice( optional(kw("inner")), seq(kw("left"), optional(kw("outer"))), seq(kw("right"), optional(kw("outer"))), seq(kw("full"), optional(kw("outer"))) ), kw("join") ), create_function_statement: ($) => seq( kw("create"), optional($.or_replace), kw("function"), $.function_signature, $.function_return, kw("as"), choice($.block, $.string), kw("language"), choice($.identifier, $.string), optional($.function_volatility), optional($.function_run_as) ), function_run_as: ($) => seq(kw("security"), choice(kw("invoker"), kw("definer"))), function_return: ($) => seq(kw("returns"), choice($._type, $.return_setof, $.return_table)), return_setof: ($) => seq(kw("setof"), $._type), return_table: ($) => seq(kw("table"), "(", commaSep1($.var_declaration), ")"), function_volatility: ($) => choice(kw("immutable"), kw("stable"), kw("volatile")), block: ($) => seq($.dollar_quote, repeat($.declarations), $.body, $.dollar_quote), body: ($) => seq(kw("begin"), repeat($._plpgsql_statement), kw("end"), optional(";")), dollar_quote: ($) => seq("$", optional($.identifier), "$"), declarations: ($) => seq(kw("declare"), repeat($.var_definition)), var_definition: ($) => seq($.var_declaration, optional(seq(":=", $._value_expression)), ";"), function_signature: ($) => seq($.identifier, $.function_parameters), function_parameters: ($) => seq( "(", commaSep($.var_declaration), optional( seq(kw("default"), field("default_value", $._value_expression)) ), ")" ), var_declaration: ($) => seq(field("name", $.identifier), field("type", $._type)), where_filter: ($) => seq(kw("where"), $._value_expression), or_replace: ($) => seq(kw("or"), kw("replace")), temporary: ($) => choice(kw("temp"), kw("temporary")), unlogged: ($) => kw("unlogged"), if_not_exists: ($) => seq(kw("if"), kw("not"), kw("exists")), if_exists: ($) => seq(kw("if"), kw("exists")), as: ($) => seq(kw("as"), $.identifier), _type: ($) => seq( choice($.predefined_types, $.identifier), optional(choice(repeat1(seq("[", "]")), kw("%rowtype"), kw("%type"))) ), // predefined_type: // | BIGINT { BigInt } // | BIT VARYING? l = type_length? { BitVarying(l) } // | BOOLEAN { Boolean } // | CHAR VARYING? l = type_length? { Char(l) } // | CHARACTER VARYING? l = type_length? { Char(l) } // | DEC p = precision_param? { Dec(p) } // | DECIMAL p = precision_param? { Decimal(p) } // | DOUBLE PRECISION { Double } // | FLOAT p = precision_param? { Float(p) } // | INT { Int } // | INTEGER { Integer } // | INTERVAL interval = interval_field? l = type_length? { Interval(interval, l) } // | NCHAR VARYING? l = type_length? { Nchar(l) } // | NUMERIC p = precision_param? { Numeric(p) } // | REAL { Real } // | SMALLINT { SmallInt } // | TEXT { Text } // (* | TIME l = type_length? (1* NOTE(chrde): what here? *1) *) // | TIME l = type_length? WITH TIME ZONE { TimeTz(l) } // | TIME l = type_length? WITHOUT TIME ZONE { Time(l) } // (* | TIMESTAMP l = type_length? (1* NOTE(chrde): what here? *1) *) // | TIMESTAMP l = type_length? WITH TIME ZONE { TimestampTz(l) } // | TIMESTAMPTZ l = type_length? { TimestampTz(l) } // | TIMESTAMP l = type_length? WITHOUT TIME ZONE { Timestamp(l) } // | VARCHAR l = type_length? { VarChar(l) } // (* | schema_qualified_name_nontype (LEFT_PAREN vex (COMMA vex)* RIGHT_PAREN)? *) // TODO(chrde): moar types!! predefined_types: ($) => choice(seq(kw("numeric"), optional($.precision))), precision: ($) => seq("(", $.number, optional(seq(",", $.number)), ")"), string: ($) => seq("'", repeat(choice(prec(1, /''/), prec(2, /[^']/))), "'"), // NOTE(chrde): taken from https://github.com/tree-sitter/tree-sitter-javascript/blob/1ddbf1588c353edab37791cdcc9f17e56fb4ea73/grammar.js#L899 comment: ($) => token( choice(seq("--", /.*/), seq("/*", /[^*]*\*+([^/*][^*]*\*+)*/, "/")) ), _value_expression: ($) => choice( $.number, $.dollar_quote_string, $.string, $.true, $.false, $.null, $.star, seq("(", $.select_statement, ")"), seq("(", commaSep1($._value_expression), ")"), $.function_call, $.array_constructor, $.op_expression, $.time_expression, // TODO(chrde): this one feels a bit hacky? perhaps move to identifier regexp seq($.identifier, ".", $.star), $.identifier ), array_constructor: ($) => seq(kw("array"), "[", commaSep($._value_expression), "]"), // TODO(chrde): it does not handle nested dollar quotes... perhaps move to an external scanner? dollar_quote_string: ($) => seq( "$", $._identifier, "$", /(([^$]+)|(%\d+\$[sI])|(\$\d+))+/, // ^ // |- matches $1 (execute ... using placeholders) // ^ // |- matches %d1s (format placeholders) // ^ // |- matches anything other than $ "$", $._identifier, "$" ), time_expression: ($) => choice( seq( $.identifier, kw("at"), kw("time"), kw("zone"), $._value_expression ), // TODO(chrde): this is plain wrong - https://www.postgresql.org/docs/13/datatype-datetime.html seq(kw("interval"), $.string, optional($._interval_fields)) ), _interval_fields: ($) => choice( kw("year"), kw("month"), kw("day"), kw("hour"), kw("minute"), kw("second"), seq(kw("year"), kw("to"), kw("month")), seq(kw("day"), kw("to"), kw("hour")), seq(kw("day"), kw("to"), kw("minute")), seq(kw("day"), kw("to"), kw("second")), seq(kw("hour"), kw("to"), kw("minute")), seq(kw("hour"), kw("to"), kw("second")), seq(kw("minute"), kw("to"), kw("second")) ), function_call: ($) => seq( $.identifier, "(", choice($.select_statement, commaSep($._value_expression)), ")" ), op_expression: ($) => choice( prec.left(12, seq($._value_expression, $.cast, $._type)), // array access prec.right(10, seq(choice($.minus, $.plus), $._value_expression)), // ^ prec.left( 8, seq($._value_expression, choice("*", "/", "%"), $._value_expression) ), prec.left( 7, seq($._value_expression, choice("-", "+"), $._value_expression) ), prec.left(6, seq($._value_expression, $.other_op, $._value_expression)), prec.left( 5, seq($._value_expression, $.contains_op, $._value_expression) ), prec.left( 4, seq($._value_expression, $.comparison_op, $._value_expression) ), prec.left( 3, seq($._value_expression, $.comparison_kw, $._value_expression) ), prec.left(3, seq($._value_expression, $.comparison_null)), prec.right(2, seq($.not, $._value_expression)), prec.left( 1, seq($._value_expression, choice($.and, $.or), $._value_expression) ) ), _list_of_identifiers: ($) => seq("(", commaSep($.identifier), ")"), // TODO(chrde): https://www.postgresql.org/docs/13/sql-syntax-lexical.html comparison_op: ($) => choice("<", ">", "=", "<=", ">=", "<>", "!="), // TODO(chrde): is there a better name other than `contains_op`? contains_op: ($) => choice(kw("between"), kw("in"), kw("like"), kw("ilike")), comparison_null: ($) => choice(kw("is null"), kw("isnull"), kw("is not null"), kw("notnull")), comparison_kw: ($) => choice(kw("is"), kw("is distinct from"), kw("is not distinct from")), // TODO(chrde): this should be a regex other_op: ($) => choice("||", "<@", "@>", "<<", ">>", "&&", "&<", "&>", "-|-"), cast: ($) => "::", minus: ($) => "-", plus: ($) => "+", not: ($) => kw("not"), and: ($) => kw("and"), or: ($) => kw("or"), true: ($) => kw("true"), false: ($) => kw("false"), null: ($) => kw("null"), star: ($) => "*", any: ($) => /.*/, number: ($) => /-?\d+/, identifier: ($) => $._identifier, _identifier: ($) => /[a-zA-Z0-9_]+(\.?[a-zA-Z0-9_]+)*/, // ^ // |- we dont want to match consecutive dots, e.g: 1..2 consists of 3 tokens }, });