// Precedence is used by the parser to determine which rule to apply when there are two rules that can be applied. // We use the PREC dict to globally define rule pprecidence const PREC = { COMMENT: 1, STRING: 2, COMMA: -1, OBJECT: -1, USER_TYPE: 1, DECLARATION: 1, ASSIGN: 0, TERNARY: 1, OR: 2, AND: 3, REL: 4, PLUS: 5, TIMES: 6, EXP: 7, TYPEOF: 8, DELETE: 8, VOID: 8, NOT: 9, NEG: 10, INC: 11, CALL: 12, NEW: 13, REVERT: 14, MEMBER: 1 } // The following is the core grammar for Solidity. It accepts Solidity smart contracts between the versions 0.4.x and 0.7.x. module.exports = grammar({ name: 'solidity', // Extras is an array of tokens that is allowed anywhere in the document. extras: $ => [ // Allow comments to be placed anywhere in the file $.comment, // Allow characters such as whitespaces to be placed anywhere in the file /[\s\uFEFF\u2060\u200B\u00A0]/ ], // The word token allows tree-sitter to appropriately handle scenario's where an identifier includes a keyword. // Documentation: https://tree-sitter.github.io/tree-sitter/creating-parsers#keywords word: $ => $.identifier, conflicts: $ => [ // The following conflicts are all due to the array type and array access expression ambiguity [$._primary_expression, $.type_name], [$._primary_expression, $._identifier_path], [$._primary_expression, $.member_expression, $._identifier_path], [$.member_expression, $._identifier_path], // This is to deal with an ambiguity due to different revert styles [$._call_arguments, $.tuple_expression], [$._parameter_list, $.fallback_receive_definition], [$._primary_expression, $.type_cast_expression], [$.pragma_value, $._solidity], [$.variable_declaration_tuple, $.tuple_expression], [$._yul_expression, $.yul_assignment], // Ambiguity: identifier ':' [$.yul_label, $.yul_identifier], // This is to deal with ambiguities arising from different fallback styles [$.fallback_receive_definition, $._function_type] ], rules: { // -- [ Program ] -- source_file: $ => seq( repeat($._source_unit), ), // -- [ Source Element ] -- _source_unit: $ => choice( $._directive, $._declaration, ), // -- [ Directives ] -- _directive: $ => choice( $.pragma_directive, $.import_directive, ), // Pragma pragma_directive: $ => seq( "pragma", choice($.solidity_pragma_token, $.any_pragma_token), $._semicolon, ), solidity_pragma_token: $ => prec(10, seq( $._solidity, repeat(seq(field("version_constraint", $._pragma_version_constraint), optional(choice("||", "-")))), )), any_pragma_token: $ => seq( $.identifier, $.pragma_value, ), _solidity: $ => prec(1, "solidity"), pragma_value: $ => prec(0, /[^;]+/), _pragma_version_constraint: $ => seq( optional($.solidity_version_comparison_operator), $.solidity_version, ), solidity_version: $ => /"?\.? ?(\d|\*)+(\. ?(\d|\*)+ ?(\.(\d|\*)+)?)?"?/, solidity_version_comparison_operator: $ => choice("<=", "<", "^", ">", ">=", "~", "="), // Import import_directive: $ => seq( 'import', choice( $._source_import, seq($._import_clause, $._from_clause) ), $._semicolon, ), _source_import: $ => seq( field('source', $.string), optional($._import_alias) ), _import_clause: $ => choice( $._single_import, $._multiple_import, ), _from_clause: $ => seq( "from", field('source', $.string) ), _single_import: $ => seq( choice("*", field("import_name", $.identifier)), optional($._import_alias) ), _multiple_import: $ => seq( '{', commaSep($._import_declaration), '}' ), _import_declaration: $ => seq( field("import_name", $.identifier), optional($._import_alias) ), _import_alias: $ => seq("as", field("alias", $.identifier)), // -- [ Declarations ] -- _declaration: $ => choice( $.contract_declaration, $.interface_declaration, $.error_declaration, $.library_declaration, $.struct_declaration, $.enum_declaration, $.function_definition, $.constant_variable_declaration, $.user_defined_type_definition, ), user_defined_type_definition: $ => seq( 'type', field("name", $.identifier), 'is', $.primitive_type, $._semicolon ), constant_variable_declaration: $ => seq( field("type", $.type_name), "constant", field("name", $.identifier), '=', field("value", $._expression), $._semicolon ), // Contract Declarations contract_declaration: $ => seq( optional('abstract'), 'contract', field("name", $.identifier), optional($._class_heritage), field('body', $.contract_body), ), error_declaration: $ => seq( 'error', field("name", $.identifier), '(', commaSep($.error_parameter), ')', $._semicolon ), error_parameter: $ => seq( field("type", $.type_name), field("name", optional($.identifier)), ), interface_declaration: $ => seq( 'interface', field("name", $.identifier), optional($._class_heritage), field('body', $.contract_body), ), library_declaration: $ => seq( 'library', field("name", $.identifier), field('body', $.contract_body), ), _class_heritage: $ => seq( "is", commaSep1($.inheritance_specifier) ), inheritance_specifier: $ => seq( field("ancestor", $.user_defined_type), optional(field("ancestor_arguments", $._call_arguments)), ), contract_body: $ => seq( "{", repeat($._contract_member), "}", ), _contract_member: $ => choice( $.function_definition, $.modifier_definition, $.error_declaration, $.state_variable_declaration, $.struct_declaration, $.enum_declaration, $.event_definition, $.using_directive, $.constructor_definition, $.fallback_receive_definition, $.user_defined_type_definition, ), struct_declaration: $ => seq( 'struct', field("name", $.identifier), '{', repeat1($.struct_member), '}', ), struct_member: $ => seq( field("type", $.type_name), field("name", $.identifier), $._semicolon ), enum_declaration: $ => seq( 'enum', field("name", $.identifier), '{', commaSep(alias($.identifier, $.enum_value)), '}', ), event_definition: $ => seq( 'event', field('name', $.identifier), $._event_parameter_list , optional('anonymous'), $._semicolon ), _event_parameter_list: $ => seq( "(", commaSep($.event_paramater), ")" ), event_paramater: $ => seq( field("type", $.type_name), optional("indexed"), optional(field("name", $.identifier)), ), using_directive: $ => seq( 'using', alias($.user_defined_type, $.type_alias), 'for', field("source", choice($.any_source_type, $.type_name)), $._semicolon ), any_source_type: $ => '*', // -- [ Statements ] -- _statement: $ => choice( $.block_statement, $.expression_statement, $.variable_declaration_statement, $.if_statement, $.for_statement, $.while_statement, $.do_while_statement, $.continue_statement, $.break_statement, $.try_statement, $.return_statement, $.emit_statement, $.assembly_statement, $.revert_statement, ), assembly_statement: $ => seq( 'assembly', optional('"evmasm"'), "{", repeat($._yul_statement), "}" ), // -- [ Yul ] -- _yul_statement: $ => choice( $.yul_block, $.yul_variable_declaration, $.yul_assignment, $.yul_function_call, $.yul_if_statement, $.yul_for_statement, $.yul_switch_statement, $.yul_leave, $.yul_break, $.yul_continue, $.yul_function_definition, $.yul_label, $._yul_literal ), yul_label: $ => seq($.identifier, ":"), yul_leave: $ => "leave", yul_break: $ => "break", yul_continue: $ => "continue", yul_identifier: $ => $.identifier, ///[a-zA-Z$_]+/, _yul_expression: $ => choice($.yul_path, $.yul_function_call, $._yul_literal), yul_path: $ => prec.left(dotSep1($.yul_identifier)), // -- Yul Literals -- _yul_literal: $ => choice( $.yul_decimal_number, $.yul_string_literal, $.yul_hex_number, $.yul_boolean, ), yul_decimal_number: $ => /0|([1-9][0-9]*)/, yul_string_literal: $ => $.string, yul_hex_number: $ => /0x[0-9A-Fa-f]*/, yul_boolean: $ => choice('true', 'false'), // -- Yul Statements -- yul_block: $ => seq('{', repeat($._yul_statement), '}'), yul_variable_declaration: $ => prec.left(PREC.DECLARATION, choice( seq('let', field("left", $.yul_identifier), optional(seq(':=', field("right", $._yul_expression)))), seq( 'let', field("left", choice( commaSep1($.yul_identifier), seq('(', commaSep1($.yul_identifier), ')') )), optional(seq(':=', field("right", $.yul_function_call)))), )), _yul_assignment_operator: $ => choice(":=", seq(":", "=")), yul_assignment: $ => prec.left(PREC.ASSIGN, choice( seq($.yul_path, $._yul_assignment_operator, $._yul_expression), seq(commaSep1($.yul_path), optional(seq($._yul_assignment_operator, $.yul_function_call))), )), yul_function_call: $ => choice( seq( field("function", choice($.yul_identifier, $.yul_evm_builtin)), '(', commaSep($._yul_expression), ')' ), field("function", $.yul_evm_builtin) ), yul_if_statement: $ => seq('if', $._yul_expression, $.yul_block), yul_for_statement: $ => seq('for', $.yul_block, $._yul_expression, $.yul_block, $.yul_block), yul_switch_statement: $ => seq( 'switch', $._yul_expression, choice( seq('default', $.yul_block), seq( repeat1(seq('case', $._yul_literal, $.yul_block)), optional(seq('default', $.yul_block)), ) ), ), yul_function_definition: $ => seq( 'function', $.yul_identifier, '(', commaSep($.yul_identifier), ')', optional(seq('->', commaSep1($.yul_identifier))), $.yul_block ), yul_evm_builtin: $ => prec(1, choice( 'stop', 'add', 'sub', 'mul', 'div', 'sdiv', 'mod', 'smod', 'exp', 'not', 'lt', 'gt', 'slt', 'sgt', 'eq', 'iszero', 'and', 'or', 'xor', 'byte', 'shl', 'shr', 'sar', 'addmod', 'mulmod', 'signextend', 'keccak256', 'pop', 'mload', 'mstore', 'mstore8', 'sload', 'sstore', 'msize', 'gas', 'address', 'balance', 'selfbalance', 'caller', 'callvalue', 'calldataload', 'calldatasize', 'calldatacopy', 'extcodesize', 'extcodecopy', 'returndatasize', 'returndatacopy', 'extcodehash', 'create', 'create2', 'call', 'callcode', 'delegatecall', 'staticcall', 'return', 'revert', 'selfdestruct', 'invalid', 'log0', 'log1', 'log2', 'log3', 'log4', 'chainid', 'origin', 'gasprice', 'blockhash', 'coinbase', 'timestamp', 'number', 'difficulty', 'gaslimit', )), // -- [ Statements ] -- _unchecked: $ => "unchecked", block_statement: $ => seq(optional($._unchecked), '{', repeat($._statement), "}"), variable_declaration_statement: $ => prec(1,seq( choice( seq($.variable_declaration, optional(seq('=', field("value", $._expression)))), seq($.variable_declaration_tuple, '=', field("value", $._expression)), ), $._semicolon )), variable_declaration: $ => seq( field("type", $.type_name), field("location", optional(choice('memory', 'storage', 'calldata'))), field('name', $.identifier) ), variable_declaration_tuple: $ => prec(3, choice( seq( '(', commaSep(optional($.variable_declaration)), ')' ), seq('var', '(', commaSep(optional($.identifier)), ')' ) )), expression_statement: $ => seq($._expression, $._semicolon), if_statement: $ => prec.right(seq( 'if', '(', field("condition", $._expression), ')', field("body", $._statement), field("else", optional( seq( 'else', field("body", $._statement) )) ), )), for_statement: $ => seq( 'for', '(', field("initial", choice($.variable_declaration_statement, $.expression_statement, $._semicolon)), field("condition", choice($.expression_statement, $._semicolon)), field("update", optional($._expression)), ')', field("body", $._statement), ), while_statement: $ => seq( 'while', '(',field("condition", $._expression), ')', field("body", $._statement), ), do_while_statement: $ => seq( 'do', field("body", $._statement), 'while', '(', field("condition", $._expression), ')', $._semicolon, ), continue_statement: $ => seq('continue', $._semicolon), break_statement: $ => seq('break', $._semicolon), revert_statement: $ => prec(PREC.REVERT, seq( 'revert', optional(field("error", $._expression)), optional(alias($._call_arguments, $.revert_arguments)), $._semicolon )), try_statement: $ => seq( 'try', field("attempt", $._expression), optional(seq('returns', $._parameter_list)), field("body", $.block_statement), repeat1($.catch_clause), ), catch_clause: $ => seq( 'catch', optional(seq(optional($.identifier), $._parameter_list)), field("body", $.block_statement), ), return_statement: $ => seq( 'return', optional($._expression), $._semicolon ), emit_statement: $ => seq( 'emit', field("name", $._expression), $._call_arguments, $._semicolon ), // -- [ Definitions ] -- // Definitions state_variable_declaration: $ => seq( field("type", $.type_name), repeat(choice( field('visibility', $.visibility), // FIXME: this also allows external "constant", $.override_specifier, $.immutable, )), field("name", $.identifier), optional(seq( '=', field("value", $._expression) )), $._semicolon ), visibility: $ => choice( 'public', 'internal', 'private', 'external', ), state_mutability: $ => choice( 'pure', 'view', 'payable' ), immutable: $ => 'immutable', override_specifier: $ => seq( 'override', optional(seq( '(', commaSep1($.user_defined_type), ')', )) ), modifier_definition: $ => seq( "modifier", field("name", $.identifier), optional($._parameter_list), repeat(choice( $.virtual, $.override_specifier, )), choice($._semicolon, field("body", $.function_body)), ), constructor_definition: $ => seq( 'constructor', $._parameter_list, repeat(choice( $.modifier_invocation, 'payable', choice('internal', 'public'), )), field('body', $.function_body), ), fallback_receive_definition: $ => seq( choice(seq( // optional("function"), choice('fallback', 'receive', 'function'), ), "function" ), // #todo: only fallback should get arguments $._parameter_list, // FIXME: We use repeat to allow for unorderedness. However, this means that the parser // accepts more than just the solidity language. The same problem exists for other definition rules. repeat(choice( $.visibility, $.modifier_invocation, $.state_mutability, $.virtual, $.override_specifier, )), choice($._semicolon, field('body', $.function_body)) ), function_definition: $ => seq( "function", field("name", $.identifier), $._parameter_list, repeat(choice( $.modifier_invocation, $.visibility, $.state_mutability, $.virtual, $.override_specifier, )), field("return_type", optional($.return_type_definition)), choice($._semicolon, field('body', $.function_body)) ), return_type_definition: $ => seq( 'returns', $._parameter_list, ), virtual: $ => "virtual", modifier_invocation: $ => seq($._identifier_path, optional($._call_arguments)), _call_arguments: $ => prec(4, seq( '(', commaSep($.call_argument), ')' ), ), call_argument: $ => choice( $._expression, seq("{", commaSep($.call_struct_argument), "}"), ), call_struct_argument: $ => seq( field("name", $.identifier), ":", field("value", $._expression) ), function_body: $ => seq( "{", repeat($._statement), "}", ), // Expressions _expression: $ => choice( $.binary_expression, $.unary_expression, $.update_expression, $.call_expression, // TODO: $.function_call_options_expression, $.payable_conversion_expression, $.meta_type_expression, $._primary_expression, $.struct_expression, $.ternary_expression, $.type_cast_expression, ), _primary_expression: $ => choice( $.parenthesized_expression, $.member_expression, $.array_access, $.slice_access, $.primitive_type, $.assignment_expression, $.augmented_assignment_expression, $.user_defined_type, $.tuple_expression, $.inline_array_expression, $.identifier, $._literal, $.new_expression, ), // TODO: back this up with official documentation type_cast_expression: $ => prec.left(seq($.primitive_type, '(', $._expression,')')), ternary_expression: $ => prec.left(seq($._expression, "?", $._expression, ':', $._expression)), // TODO: make sure call arguments are part of solidity new_expression: $ => prec.left(seq('new', field("name", $.type_name), optional($._call_arguments))), tuple_expression: $ => prec(1, seq('(', commaSep(optional($._expression)), ')' )), inline_array_expression: $ => seq('[', commaSep($._expression), ']' ), binary_expression: $ => choice( ...[ ['&&', PREC.AND], ['||', PREC.OR], ['>>', PREC.TIMES], ['>>>', PREC.TIMES], ['<<', PREC.TIMES], ['&', PREC.AND], ['^', PREC.OR], ['|', PREC.OR], ['+', PREC.PLUS], ['-', PREC.PLUS], ['*', PREC.TIMES], ['/', PREC.TIMES], ['%', PREC.TIMES], ['**', PREC.EXP], ['<', PREC.REL], ['<=', PREC.REL], ['==', PREC.REL], ['!=', PREC.REL], ['!==', PREC.REL], ['>=', PREC.REL], ['>', PREC.REL], ].map(([operator, precedence]) => prec.left(precedence, seq( field('left', $._expression), field('operator', operator), field('right', $._expression) )) ) ), unary_expression: $ => choice(...[ ['!', PREC.NOT], ['~', PREC.NOT], ['-', PREC.NEG], ['+', PREC.NEG], ['delete', PREC.DELETE], ].map(([operator, precedence]) => prec.left(precedence, seq( field('operator', operator), field('argument', $._expression) )) )), update_expression: $ => prec.left(PREC.INC, choice( seq( field('argument', $._expression), field('operator', choice('++', '--')) ), seq( field('operator', choice('++', '--')), field('argument', $._expression) ), )), member_expression: $ => prec.dynamic(1, seq( field('object', choice( $._expression, $.identifier, )), '.', field('property', $.identifier) )), array_access: $ => seq( field('base', $._expression), '[', optional(field('index', $._expression)), ']' ), slice_access: $ => seq( field('base', $._expression), '[', optional(field('from', $._expression)), ':', optional(field('to', $._expression)), ']' ), struct_expression: $ => seq( field("type", $._expression), "{", commaSep($.struct_field_assignment), "}" ), struct_field_assignment: $ => seq( field("name", $.identifier), ":", field("value", $._expression), ), parenthesized_expression: $ => prec(2, seq('(', $._expression, ')')), assignment_expression: $ => prec.right(PREC.ASSIGN, seq( field('left', $._expression), '=', field('right', $._expression) )), augmented_assignment_expression: $ => prec.right(PREC.ASSIGN, seq( field('left', $._expression), choice('+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=', '>>=', '>>>=', '<<=',), field('right', $._expression) )), call_expression: $ => prec.right(PREC.CALL, seq( field("function", $._expression), $._call_arguments )), payable_conversion_expression: $ => seq('payable', $._call_arguments), meta_type_expression: $ => seq('type', '(', $.type_name, ')'), type_name: $ => choice( $.primitive_type, $.user_defined_type, $._mapping, $._array_type, $._function_type, ), _array_type: $ => prec(1, seq($.type_name, '[', optional($._expression), ']')), _function_type: $ => prec.right(seq( 'function', field("parameters", $._parameter_list), repeat(choice( $.visibility, $.state_mutability, )), optional($._return_parameters), )), _parameter_list: $ => seq( '(', commaSep($.parameter), ')' ), _return_parameters: $ => seq( 'returns', '(', commaSep1(alias($._nameless_parameter, $.return_parameter)), ')' ), _nameless_parameter: $ => seq( field("type", $.type_name), field("location", optional($._storage_location)), ), parameter: $ => seq( field("type", $.type_name), optional(field("location", $._storage_location)), optional(field("name", $.identifier)), ), _storage_location: $ => choice( 'memory', 'storage', 'calldata' ), user_defined_type: $ => $._identifier_path, _identifier_path: $ => prec.left(dotSep1( $.identifier)), _mapping: $ => seq( 'mapping', '(', field("key_type", $._mapping_key), '=>', field("value_type", $.type_name), ')', ), _mapping_key: $ => choice( $.primitive_type, $.user_defined_type ), primitive_type: $ => prec.left(choice( seq('address', optional('payable')), 'bool', 'string', 'var', $._int, $._uint, $._bytes, $._fixed, $._ufixed, )), _int: $ => choice ( 'int', 'int8', 'int16', 'int24', 'int32', 'int40', 'int48', 'int56', 'int64', 'int72', 'int80', 'int88', 'int96', 'int104', 'int112', 'int120', 'int128', 'int136', 'int144', 'int152', 'int160', 'int168', 'int176', 'int184', 'int192', 'int200', 'int208', 'int216', 'int224', 'int232', 'int240', 'int248', 'int256' ), _uint: $ => choice ( 'uint', 'uint8', 'uint16', 'uint24', 'uint32', 'uint40', 'uint48', 'uint56', 'uint64', 'uint72', 'uint80', 'uint88', 'uint96', 'uint104', 'uint112', 'uint120', 'uint128', 'uint136', 'uint144', 'uint152', 'uint160', 'uint168', 'uint176', 'uint184', 'uint192', 'uint200', 'uint208', 'uint216', 'uint224', 'uint232', 'uint240', 'uint248', 'uint256' ), _bytes: $ => choice ( 'byte', 'bytes', 'bytes1', 'bytes2', 'bytes3', 'bytes4', 'bytes5', 'bytes6', 'bytes7', 'bytes8', 'bytes9', 'bytes10', 'bytes11', 'bytes12', 'bytes13', 'bytes14', 'bytes15', 'bytes16', 'bytes17', 'bytes18', 'bytes19', 'bytes20', 'bytes21', 'bytes22', 'bytes23', 'bytes24', 'bytes25', 'bytes26', 'bytes27', 'bytes28', 'bytes29', 'bytes30', 'bytes31', 'bytes32' ), _fixed: $ => choice ( 'fixed', /fixed([0-9]+)x([0-9]+)/ ), _ufixed: $ => choice ( 'ufixed', /ufixed([0-9]+)x([0-9]+)/ ), _semicolon: $ => ';', identifier: $ => /[a-zA-Z$_][a-zA-Z0-9$_]*/, number: $ => /\d+/, _literal: $ => choice( $.string_literal, $.number_literal, $.boolean_literal, $.hex_string_literal, $.unicode_string_literal, ), string_literal: $ => prec.left(repeat1($.string)), number_literal: $ => seq(choice($._decimal_number, $._hex_number), optional($.number_unit)), _decimal_number: $ => choice( /(\d|_)+(\.(\d|_)+)?([eE](-)?(\d|_)+)?/, /\.(\d|_)+([eE](-)?(\d|_)+)?/, ), _hex_number: $ => prec(10, /0[xX]([a-fA-F0-9][a-fA-F0-9]?_?)+/), // _hex_number: $ => seq(/0[xX]/, optional(optionalDashSeparation($._hex_digit))), _hex_digit: $ => /([a-fA-F0-9][a-fA-F0-9])/, number_unit: $ => choice( 'wei','szabo', 'finney', 'gwei', 'ether', 'seconds', 'minutes', 'hours', 'days', 'weeks', 'years' ), true: $ => "true", false: $ => "false", boolean_literal: $ => choice($.true, $.false), hex_string_literal: $ => prec.left(repeat1(seq( 'hex', choice( seq('"', optional(optionalDashSeparation($._hex_digit)), '"'), seq("'", optional(optionalDashSeparation($._hex_digit)), "'"), )))), _escape_sequence: $ => token.immediate(seq( '\\', choice( /[^xu0-7]/, /[0-7]{1,3}/, /x[0-9a-fA-F]{2}/, /u[0-9a-fA-F]{4}/, /u{[0-9a-fA-F]+}/ ) )), _single_quoted_unicode_char: $ => token.immediate(prec(PREC.STRING, /[^'\\\n]+|\\\r?\n/)), _double_quoted_unicode_char: $ => token.immediate(prec(PREC.STRING, /[^"\\\n]+|\\\r?\n/)), unicode_string_literal: $ => prec.left(repeat1(seq( 'unicode', choice( seq('"', repeat($._double_quoted_unicode_char), '"'), seq("'", repeat($._single_quoted_unicode_char), "'"), )))), string: $ => choice( seq( '"', repeat(choice( $._string_immediate_elt_inside_double_quote, $._escape_sequence )), '"' ), seq( "'", repeat(choice( $._string_immediate_elt_inside_quote, $._escape_sequence )), "'" ) ), // We need to name those elts for ocaml-tree-sitter-semgrep. _string_immediate_elt_inside_double_quote: $ => token.immediate(prec(PREC.STRING, /[^"\\\n]+|\\\r?\n/)), _string_immediate_elt_inside_quote: $ => token.immediate(prec(PREC.STRING, /[^'\\\n]+|\\\r?\n/)), // Based on: https://github.com/tree-sitter/tree-sitter-c/blob/master/grammar.js#L965 comment: $ => token( prec(PREC.COMMENT, choice( seq('//', /([^\r\n])*/), seq( '/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/' ) ) ) ), } } ); function dotSep1(rule) { return seq( rule, repeat( seq( '.', rule ) ), ); } function dotSep(rule) { return optional(dotSep1(rule)) } function commaSep1(rule) { return seq( rule, repeat( seq( ',', rule ) ), optional(','), ); } function commaSep(rule) { return optional(commaSep1(rule)); } function optionalDashSeparation(rule) { return seq( rule, repeat( seq( optional('_'), rule ) ), ); }