Merge branch 'optional-function-args'

This commit is contained in:
Bartłomiej Pluta
2019-07-15 20:26:48 +02:00
16 changed files with 197 additions and 91 deletions

View File

@@ -1,4 +1,6 @@
from smnp.ast.node.operator import BinaryOperator from smnp.ast.node.model import Node
from smnp.ast.node.none import NoneNode
from smnp.ast.node.operator import BinaryOperator, Operator
from smnp.ast.node.term import TermParser from smnp.ast.node.term import TermParser
from smnp.ast.parser import Parser from smnp.ast.parser import Parser
from smnp.token.type import TokenType from smnp.token.type import TokenType
@@ -20,7 +22,34 @@ class Or(BinaryOperator):
pass pass
def ExpressionParser(input): class Loop(BinaryOperator):
def __init__(self, pos):
super().__init__(pos)
self.children.append(NoneNode())
@property
def parameters(self):
return self[3]
@parameters.setter
def parameters(self, value):
self[3] = value
@classmethod
def loop(cls, left, parameters, operator, right):
node = cls(left.pos)
node.left = left
node.parameters = parameters
node.operator = operator
node.right = right
return node
class LoopParameters(Node):
pass
def ExpressionWithoutLoopParser(input):
expr1 = Parser.leftAssociativeOperatorParser( expr1 = Parser.leftAssociativeOperatorParser(
TermParser, TermParser,
[TokenType.PLUS, TokenType.MINUS], [TokenType.PLUS, TokenType.MINUS],
@@ -49,3 +78,34 @@ def ExpressionParser(input):
lambda left, op, right: Or.withValues(left, op, right) lambda left, op, right: Or.withValues(left, op, right)
)(input) )(input)
def LoopParser(input):
from smnp.ast.node.identifier import IdentifierLiteralParser
from smnp.ast.node.iterable import abstractIterableParser
from smnp.ast.node.statement import StatementParser
loopParameters = Parser.allOf(
Parser.terminal(TokenType.AS),
Parser.oneOf(
Parser.wrap(IdentifierLiteralParser, lambda id: LoopParameters.withChildren([id], id.pos)),
abstractIterableParser(LoopParameters, TokenType.OPEN_PAREN, TokenType.CLOSE_PAREN, IdentifierLiteralParser)
),
createNode=lambda asKeyword, parameters: parameters,
name="loop parameters"
)
return Parser.allOf(
ExpressionWithoutLoopParser,
Parser.optional(loopParameters),
Parser.terminal(TokenType.DASH, createNode=Operator.withValue),
StatementParser,
createNode=Loop.loop,
name="dash-loop"
)(input)
def ExpressionParser(input):
return Parser.oneOf(
LoopParser,
ExpressionWithoutLoopParser
)(input)

View File

@@ -1,6 +1,3 @@
from smnp.ast.node.iterable import abstractIterableParser
from smnp.ast.node.model import Node
from smnp.ast.node.none import NoneNode
from smnp.ast.node.operator import BinaryOperator, Operator, UnaryOperator from smnp.ast.node.operator import BinaryOperator, Operator, UnaryOperator
from smnp.ast.node.unit import UnitParser from smnp.ast.node.unit import UnitParser
from smnp.ast.parser import Parser from smnp.ast.parser import Parser
@@ -13,38 +10,8 @@ class NotOperator(UnaryOperator):
class Power(BinaryOperator): class Power(BinaryOperator):
pass pass
class Loop(BinaryOperator):
def __init__(self, pos):
super().__init__(pos)
self.children.append(NoneNode())
@property
def parameters(self):
return self[3]
@parameters.setter
def parameters(self, value):
self[3] = value
@classmethod
def loop(cls, left, parameters, operator, right):
node = cls(left.pos)
node.left = left
node.parameters = parameters
node.operator = operator
node.right = right
return node
class LoopParameters(Node):
pass
def FactorParser(input): def FactorParser(input):
from smnp.ast.node.expression import ExpressionParser from smnp.ast.node.expression import ExpressionParser
from smnp.ast.node.statement import StatementParser
from smnp.ast.node.identifier import IdentifierLiteralParser
parentheses = Parser.allOf( parentheses = Parser.allOf(
Parser.terminal(TokenType.OPEN_PAREN), Parser.terminal(TokenType.OPEN_PAREN),
@@ -75,27 +42,7 @@ def FactorParser(input):
name="not" name="not"
) )
loopParameters = Parser.allOf(
Parser.terminal(TokenType.AS),
Parser.oneOf(
Parser.wrap(IdentifierLiteralParser, lambda id: LoopParameters.withChildren([id], id.pos)),
abstractIterableParser(LoopParameters, TokenType.OPEN_PAREN, TokenType.CLOSE_PAREN, IdentifierLiteralParser)
),
createNode=lambda asKeyword, parameters: parameters,
name="loop parameters"
)
loopFactor = Parser.allOf(
powerFactor,
Parser.optional(loopParameters),
Parser.terminal(TokenType.DASH, createNode=Operator.withValue),
StatementParser,
createNode=Loop.loop,
name="dash-loop"
)
return Parser.oneOf( return Parser.oneOf(
loopFactor,
notOperator, notOperator,
powerFactor, powerFactor,
name="factor" name="factor"

View File

@@ -1,4 +1,5 @@
from smnp.ast.node.block import BlockParser from smnp.ast.node.block import BlockParser
from smnp.ast.node.expression import ExpressionParser
from smnp.ast.node.identifier import IdentifierLiteralParser from smnp.ast.node.identifier import IdentifierLiteralParser
from smnp.ast.node.iterable import abstractIterableParser from smnp.ast.node.iterable import abstractIterableParser
from smnp.ast.node.model import Node from smnp.ast.node.model import Node
@@ -16,7 +17,7 @@ class Argument(Node):
def __init__(self, pos): def __init__(self, pos):
super().__init__(pos) super().__init__(pos)
self.children = [NoneNode(), NoneNode(), False] self.children = [NoneNode(), NoneNode(), False, NoneNode()]
@property @property
def type(self): def type(self):
@@ -48,6 +49,14 @@ class Argument(Node):
self[2] = value self[2] = value
@property
def optionalValue(self):
return self[3]
@optionalValue.setter
def optionalValue(self, value):
self[3] = value
class VarargNode(Node): class VarargNode(Node):
pass pass
@@ -90,7 +99,7 @@ class FunctionDefinition(Node):
return node return node
def ArgumentParser(input): def RegularArgumentParser(input):
def createNode(type, variable, vararg): def createNode(type, variable, vararg):
pos = type.pos if isinstance(type, Type) else variable.pos pos = type.pos if isinstance(type, Type) else variable.pos
node = Argument(pos) node = Argument(pos)
@@ -104,6 +113,33 @@ def ArgumentParser(input):
Parser.doAssert(IdentifierLiteralParser, "argument name"), Parser.doAssert(IdentifierLiteralParser, "argument name"),
Parser.optional(Parser.terminal(TokenType.DOTS, lambda val, pos: True)), Parser.optional(Parser.terminal(TokenType.DOTS, lambda val, pos: True)),
createNode=createNode, createNode=createNode,
name="regular function argument"
)(input)
def OptionalArgumentParser(input):
def createNode(type, variable, _, optional):
pos = type.pos if isinstance(type, Type) else variable.pos
node = Argument(pos)
node.type = type
node.variable = variable
node.optionalValue = optional
return node
return Parser.allOf(
Parser.optional(TypeParser),
Parser.doAssert(IdentifierLiteralParser, "argument name"),
Parser.terminal(TokenType.ASSIGN),
Parser.doAssert(ExpressionParser, "expression"),
createNode=createNode,
name="optional function argument"
)(input)
def ArgumentParser(input):
return Parser.oneOf(
OptionalArgumentParser,
RegularArgumentParser,
name="function argument" name="function argument"
)(input) )(input)

View File

@@ -1,5 +1,6 @@
from smnp.ast.node.model import Node from smnp.ast.node.model import Node
from smnp.ast.parser import Parser from smnp.ast.parser import Parser
from smnp.token.type import TokenType
class Statement(Node): class Statement(Node):
@@ -13,11 +14,24 @@ def StatementParser(input):
from smnp.ast.node.ret import ReturnParser from smnp.ast.node.ret import ReturnParser
from smnp.ast.node.throw import ThrowParser from smnp.ast.node.throw import ThrowParser
return Parser.oneOf( return withSemicolon(
IfElseStatementParser, Parser.oneOf(
ExpressionParser, IfElseStatementParser,
BlockParser, ExpressionParser, # Must be above BlockParser because of Map's syntax with curly braces
ReturnParser, BlockParser,
ThrowParser, ReturnParser,
name="statement" ThrowParser,
)(input) name="statement"
), optional=True)(input)
def withSemicolon(parser, optional=False, doAssert=False):
semicolonParser = Parser.optional(Parser.terminal(TokenType.SEMICOLON)) if optional else Parser.terminal(
TokenType.SEMICOLON, doAssert=doAssert)
return Parser.allOf(
parser,
semicolonParser,
createNode=lambda stmt, semicolon: stmt,
name="semicolon" + "?" if optional else ""
)

View File

@@ -40,7 +40,8 @@ class Environment():
if method.typeSignature.check([object])[0] and method.name == name: #Todo sprawdzic sygnature typu if method.typeSignature.check([object])[0] and method.name == name: #Todo sprawdzic sygnature typu
signatureCheckresult = method.signature.check(args) signatureCheckresult = method.signature.check(args)
if signatureCheckresult[0]: if signatureCheckresult[0]:
self.scopes.append({argName: argValue for argName, argValue in zip(method.arguments, list(signatureCheckresult[1:]))}) self.scopes.append(method.defaultArgs)
self.scopes[-1].update({argName: argValue for argName, argValue in zip(method.arguments, list(signatureCheckresult[1:]))})
self.scopes[-1][method.alias] = object self.scopes[-1][method.alias] = object
self.callStack.append(CallStackItem(name)) self.callStack.append(CallStackItem(name))
result = Type.void() result = Type.void()
@@ -80,7 +81,8 @@ class Environment():
if function.name == name: if function.name == name:
signatureCheckresult = function.signature.check(args) signatureCheckresult = function.signature.check(args)
if signatureCheckresult[0]: if signatureCheckresult[0]:
self.scopes.append({ argName: argValue for argName, argValue in zip(function.arguments, list(signatureCheckresult[1:])) }) self.scopes.append(function.defaultArgs)
self.scopes[-1].update({ argName: argValue for argName, argValue in zip(function.arguments, list(signatureCheckresult[1:])) })
self.callStack.append(CallStackItem(name)) self.callStack.append(CallStackItem(name))
result = Type.void() result = Type.void()
try: try:
@@ -93,11 +95,11 @@ class Environment():
raise IllegalFunctionInvocationException(f"{function.name}{function.signature.string}", f"{name}{argsTypesToString(args)}") raise IllegalFunctionInvocationException(f"{function.name}{function.signature.string}", f"{name}{argsTypesToString(args)}")
return (False, None) return (False, None)
def addCustomFunction(self, name, signature, arguments, body): def addCustomFunction(self, name, signature, arguments, body, defaultArguments):
if len([fun for fun in self.functions + self.customFunctions if fun.name == name]) > 0: if len([fun for fun in self.functions + self.customFunctions if fun.name == name]) > 0:
raise RuntimeException(f"Cannot redeclare function '{name}'", None) raise RuntimeException(f"Cannot redeclare function '{name}'", None)
self.customFunctions.append(CustomFunction(name, signature, arguments, body)) self.customFunctions.append(CustomFunction(name, signature, arguments, body, defaultArguments))
# TODO: # TODO:
# There is still problem with checking existing of generic types, like lists: # There is still problem with checking existing of generic types, like lists:
@@ -108,14 +110,14 @@ class Environment():
# function foo() { return 2 } # function foo() { return 2 }
# } # }
# Then calling [1, 2, 3, 4].foo() will produce 1, when the second method is more suitable # Then calling [1, 2, 3, 4].foo() will produce 1, when the second method is more suitable
def addCustomMethod(self, typeSignature, alias, name, signature, arguments, body): def addCustomMethod(self, typeSignature, alias, name, signature, arguments, body, defaultArguments):
if len([m for m in self.methods if m.name == name and m.signature.matchers[0] == typeSignature.matchers[0]]) > 0: if len([m for m in self.methods if m.name == name and m.signature.matchers[0] == typeSignature.matchers[0]]) > 0:
raise RuntimeException(f"Cannot redeclare method '{name}' for type '{typeSignature.matchers[0]}'", None) raise RuntimeException(f"Cannot redeclare method '{name}' for type '{typeSignature.matchers[0]}'", None)
if len([m for m in self.customMethods if m.name == name and m.typeSignature.matchers[0] == typeSignature.matchers[0]]) > 0: if len([m for m in self.customMethods if m.name == name and m.typeSignature.matchers[0] == typeSignature.matchers[0]]) > 0:
raise RuntimeException(f"Cannot redeclare method '{name}' for type '{typeSignature.matchers[0]}'", None) raise RuntimeException(f"Cannot redeclare method '{name}' for type '{typeSignature.matchers[0]}'", None)
self.customMethods.append(CustomMethod(typeSignature, alias, name, signature, arguments, body)) self.customMethods.append(CustomMethod(typeSignature, alias, name, signature, arguments, body, defaultArguments))
def findVariable(self, name, type=None, pos=None): def findVariable(self, name, type=None, pos=None):
for scope in reversed(self.scopes): for scope in reversed(self.scopes):
@@ -175,18 +177,20 @@ class CallStackItem:
class CustomFunction: class CustomFunction:
def __init__(self, name, signature, arguments, body): def __init__(self, name, signature, arguments, body, defaultArgs):
self.name = name self.name = name
self.signature = signature self.signature = signature
self.arguments = arguments self.arguments = arguments
self.body = body self.body = body
self.defaultArgs = defaultArgs
class CustomMethod: class CustomMethod:
def __init__(self, typeSignature, alias, name, signature, arguments, body): def __init__(self, typeSignature, alias, name, signature, arguments, body, defaultArgs):
self.typeSignature = typeSignature self.typeSignature = typeSignature
self.alias = alias self.alias = alias
self.name = name self.name = name
self.signature = signature self.signature = signature
self.arguments = arguments self.arguments = arguments
self.body = body self.body = body
self.defaultArgs = defaultArgs

View File

@@ -12,6 +12,9 @@ class Signature:
def varargSignature(varargMatcher, *basicSignature, wrapVarargInValue=False): def varargSignature(varargMatcher, *basicSignature, wrapVarargInValue=False):
def check(args): def check(args):
if any([ matcher.optional for matcher in [ varargMatcher, *basicSignature ]]):
raise RuntimeError("Vararg signature can't have optional arguments")
if len(basicSignature) > len(args): if len(basicSignature) > len(args):
return doesNotMatchVararg(basicSignature) return doesNotMatchVararg(basicSignature)
@@ -38,7 +41,7 @@ def doesNotMatchVararg(basicSignature):
def signature(*signature): def signature(*signature):
def check(args): def check(args):
if len(signature) != len(args): if len(args) > len(signature) or len(args) < len([ matcher for matcher in signature if not matcher.optional ]):
return doesNotMatch(signature) return doesNotMatch(signature)
for s, a in zip(signature, args): for s, a in zip(signature, args):
@@ -52,6 +55,12 @@ def signature(*signature):
return Signature(check, string, signature) return Signature(check, string, signature)
def optional(matcher):
matcher.optional = True
matcher.string += "?"
return matcher
def doesNotMatch(sign): def doesNotMatch(sign):
return (False, *[None for n in sign]) return (False, *[None for n in sign])

View File

@@ -9,9 +9,6 @@ def main():
try: try:
stdLibraryEnv = loadStandardLibrary() stdLibraryEnv = loadStandardLibrary()
Interpreter.interpretFile(sys.argv[1], printTokens=False, printAst=False, execute=True, baseEnvironment=stdLibraryEnv) Interpreter.interpretFile(sys.argv[1], printTokens=False, printAst=False, execute=True, baseEnvironment=stdLibraryEnv)
#draft()
#tokens = tokenize(['function a(b...) { x+y}'])
#FunctionDefinitionParser(tokens).node.print()
except SmnpException as e: except SmnpException as e:
print(e.message()) print(e.message())

View File

@@ -1,4 +1,4 @@
from smnp.module.iterable.function import combine, map, range, get from smnp.module.iterable.function import combine, map, get
functions = [ combine.function, map.function, range.function ] functions = [ combine.function, map.function ]
methods = [ get.function ] methods = [ get.function ]

View File

@@ -1,6 +1,6 @@
from smnp.ast.node.condition import IfElse from smnp.ast.node.condition import IfElse
from smnp.ast.node.expression import Sum, Relation, And, Or from smnp.ast.node.expression import Sum, Relation, And, Or, Loop
from smnp.ast.node.factor import NotOperator, Power, Loop from smnp.ast.node.factor import NotOperator, Power
from smnp.ast.node.identifier import FunctionCall, Assignment from smnp.ast.node.identifier import FunctionCall, Assignment
from smnp.ast.node.term import Product from smnp.ast.node.term import Product
from smnp.ast.node.unit import MinusOperator, Access from smnp.ast.node.unit import MinusOperator, Access
@@ -50,3 +50,15 @@ def expressionEvaluator(doAssert=False):
return evaluateExpression return evaluateExpression
def expressionEvaluatorWithMatcher(matcher, exceptionProvider, doAssert=True):
def evaluate(node, environment):
value = expressionEvaluator(doAssert=doAssert)(node, environment).value
if not matcher.match(value):
raise exceptionProvider(value)
return value
return evaluate

View File

@@ -32,8 +32,8 @@ class ExtendEvaluator(Evaluator):
@classmethod @classmethod
def _evaluateMethodDefinition(cls, node, environment, type, variable): def _evaluateMethodDefinition(cls, node, environment, type, variable):
name = node.name.value name = node.name.value
signature = argumentsNodeToMethodSignature(node.arguments) defaultArguments, signature = argumentsNodeToMethodSignature(node.arguments, environment)
arguments = [arg.variable.value for arg in node.arguments] arguments = [arg.variable.value for arg in node.arguments]
body = node.body body = node.body
environment.addCustomMethod(type, variable, name, signature, arguments, body) environment.addCustomMethod(type, variable, name, signature, arguments, body, defaultArguments)

View File

@@ -25,10 +25,10 @@ class FunctionDefinitionEvaluator(Evaluator):
def evaluator(cls, node, environment): def evaluator(cls, node, environment):
try: try:
name = node.name.value name = node.name.value
signature = argumentsNodeToMethodSignature(node.arguments) defaultArguments, signature = argumentsNodeToMethodSignature(node.arguments, environment)
arguments = [ arg.variable.value for arg in node.arguments ] arguments = [ arg.variable.value for arg in node.arguments ]
body = node.body body = node.body
environment.addCustomFunction(name, signature, arguments, body) environment.addCustomFunction(name, signature, arguments, body, defaultArguments)
except RuntimeException as e: except RuntimeException as e:
raise updatePos(e, node) raise updatePos(e, node)

View File

@@ -2,7 +2,8 @@ from smnp.ast.node import type as ast
from smnp.ast.node.none import NoneNode from smnp.ast.node.none import NoneNode
from smnp.ast.node.type import TypesList from smnp.ast.node.type import TypesList
from smnp.error.runtime import RuntimeException from smnp.error.runtime import RuntimeException
from smnp.function.signature import varargSignature, signature from smnp.function.signature import varargSignature, signature, optional
from smnp.runtime.evaluators.expression import expressionEvaluator, expressionEvaluatorWithMatcher
from smnp.runtime.tools.error import updatePos from smnp.runtime.tools.error import updatePos
from smnp.type.model import Type from smnp.type.model import Type
from smnp.type.signature.matcher.list import listOfMatchers from smnp.type.signature.matcher.list import listOfMatchers
@@ -10,11 +11,19 @@ from smnp.type.signature.matcher.map import mapOfMatchers
from smnp.type.signature.matcher.type import allTypes, oneOf, ofType from smnp.type.signature.matcher.type import allTypes, oneOf, ofType
def argumentsNodeToMethodSignature(node): def evaluateDefaultArguments(node, environment):
defaultValues = { arg.variable.value: expressionEvaluator(doAssert=True)(arg.optionalValue, environment).value for arg in node.children if type(arg.optionalValue) != NoneNode }
return defaultValues
def argumentsNodeToMethodSignature(node, environment):
try: try:
sign = [] sign = []
vararg = None vararg = None
argumentsCount = len(node.children) argumentsCount = len(node.children)
checkPositionOfOptionalArguments(node)
defaultArgs = {}
for i, child in enumerate(node.children): for i, child in enumerate(node.children):
matchers = { matchers = {
ast.Type: (lambda c: c.type, typeMatcher), ast.Type: (lambda c: c.type, typeMatcher),
@@ -22,19 +31,34 @@ def argumentsNodeToMethodSignature(node):
TypesList: (lambda c: c, multipleTypeMatcher) TypesList: (lambda c: c, multipleTypeMatcher)
} }
evaluatedMatcher = matchers[type(child.type)][1](matchers[type(child.type)][0](child)) evaluatedMatcher = matchers[type(child.type)][1](matchers[type(child.type)][0](child))
if child.vararg: if child.vararg:
if i != argumentsCount - 1: if i != argumentsCount - 1:
raise RuntimeException("Vararg must be the last argument in signature", child.pos) raise RuntimeException("Vararg must be the last argument in signature", child.pos)
vararg = evaluatedMatcher vararg = evaluatedMatcher
else: else:
if type(child.optionalValue) != NoneNode:
defaultArgs[child.variable.value] = expressionEvaluatorWithMatcher(
evaluatedMatcher,
exceptionProvider=lambda value: RuntimeException(
f"Value '{value.stringify()}' doesn't match declared type: {evaluatedMatcher.string}", child.optionalValue.pos)
)(child.optionalValue, environment)
evaluatedMatcher = optional(evaluatedMatcher)
sign.append(evaluatedMatcher) sign.append(evaluatedMatcher)
return defaultArgs, (varargSignature(vararg, *sign, wrapVarargInValue=True) if vararg is not None else signature(*sign))
return varargSignature(vararg, *sign, wrapVarargInValue=True) if vararg is not None else signature(*sign)
except RuntimeException as e: except RuntimeException as e:
raise updatePos(e, node) raise updatePos(e, node)
def checkPositionOfOptionalArguments(node):
firstOptional = next((i for i, v in enumerate(node.children) if type(v.optionalValue) != NoneNode), None) #next(filter(lambda arg: type(arg.optionalValue) != NoneNode, node.children), None)
if firstOptional is not None:
regularAfterOptional = next((i for i, v in enumerate(node.children[firstOptional:]) if type(v.optionalValue) == NoneNode), None)
if regularAfterOptional is not None:
raise RuntimeException(f"Optional arguments should be declared at the end of the arguments list", node.children[regularAfterOptional].pos)
def multipleTypeMatcher(typeNode): def multipleTypeMatcher(typeNode):
subSignature = [] subSignature = []

View File

@@ -27,6 +27,7 @@ tokenizers = (
defaultTokenizer(TokenType.CLOSE_SQUARE), defaultTokenizer(TokenType.CLOSE_SQUARE),
defaultTokenizer(TokenType.OPEN_ANGLE), defaultTokenizer(TokenType.OPEN_ANGLE),
defaultTokenizer(TokenType.CLOSE_ANGLE), defaultTokenizer(TokenType.CLOSE_ANGLE),
defaultTokenizer(TokenType.SEMICOLON),
defaultTokenizer(TokenType.ASTERISK), defaultTokenizer(TokenType.ASTERISK),
defaultTokenizer(TokenType.ASSIGN), defaultTokenizer(TokenType.ASSIGN),
defaultTokenizer(TokenType.COMMA), defaultTokenizer(TokenType.COMMA),

View File

@@ -12,6 +12,7 @@ class TokenType(Enum):
CLOSE_SQUARE = ']' CLOSE_SQUARE = ']'
OPEN_ANGLE = '<' OPEN_ANGLE = '<'
CLOSE_ANGLE = '>' CLOSE_ANGLE = '>'
SEMICOLON = ';'
ASTERISK = '*' ASTERISK = '*'
ASSIGN = '=' ASSIGN = '='
ARROW = '->' ARROW = '->'

View File

@@ -3,6 +3,7 @@ class Matcher:
self.type = objectType self.type = objectType
self.matcher = matcher self.matcher = matcher
self.string = string self.string = string
self.optional = False
def match(self, value): def match(self, value):
if self.type is not None and self.type != value.type: if self.type is not None and self.type != value.type:

View File

@@ -25,4 +25,4 @@ def oneOf(*matchers):
def check(value): def check(value):
return any(matcher.match(value) for matcher in matchers) return any(matcher.match(value) for matcher in matchers)
return Matcher(None, check, f"<{', '.join(m.string for m in matchers)}>") return Matcher(None, check, f"<{', '.join(m.string for m in matchers)}>")