Add in IR generation
This commit is contained in:
parent
92d458de85
commit
c27852daba
|
@ -6,7 +6,6 @@ from functools import reduce
|
|||
from re import compile, Pattern
|
||||
|
||||
from lex import Lexeme, tokenize
|
||||
from parse import Action
|
||||
|
||||
from typing import Any, Callable, Collection, Mapping, Sequence, Tuple, TypeAlias
|
||||
|
||||
|
|
24
genir.py
Normal file
24
genir.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from emis_funky_funktions import *
|
||||
from typing import *
|
||||
|
||||
from silly_thing import *
|
||||
from pattern import lex_and_parse_pattern
|
||||
from ir import Function, Application, Int, Variable
|
||||
|
||||
import json
|
||||
|
||||
JsonType: TypeAlias = 'Mapping[str, JsonType] | Sequence[JsonType] | int | str'
|
||||
|
||||
def json_to_ir(j: JsonType) -> Expression:
|
||||
if isinstance(j, Mapping):
|
||||
return Function(tuple(
|
||||
#TODO handle parse errors
|
||||
(unwrap_r(lex_and_parse_pattern(k)), json_to_ir(v))
|
||||
for (k, v) in j.items()
|
||||
))
|
||||
elif isinstance(j, str):
|
||||
return Variable(j)
|
||||
elif isinstance(j, Sequence):
|
||||
return Application([json_to_ir(e) for e in j])
|
||||
else:
|
||||
return Int(j)
|
281
ir.py
Normal file
281
ir.py
Normal file
|
@ -0,0 +1,281 @@
|
|||
from emis_funky_funktions import *
|
||||
|
||||
from typing import Mapping, Sequence, Tuple, TypeAlias
|
||||
|
||||
|
||||
Expression: TypeAlias = 'Function | Application | Int | Variable | Builtin'
|
||||
Pattern: TypeAlias = 'NamePattern | IntPattern | SPattern | IgnorePattern'
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NamePattern:
|
||||
"""
|
||||
A pattern which always succeeds to match, and binds a whole expression to a name
|
||||
"""
|
||||
name: str
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
"""
|
||||
return var == self.name
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> NamePattern('my_var').match(Int(1))
|
||||
Some((('my_var', 1),))
|
||||
"""
|
||||
return Some(((self.name, e),))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IgnorePattern:
|
||||
"""
|
||||
A pattern which always succeeds to match, but binds nothing
|
||||
"""
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
|
||||
For an `IgnorePattern` this is always false
|
||||
"""
|
||||
return False
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> IgnorePattern().match(Int(1))
|
||||
Some(())
|
||||
"""
|
||||
return Some(tuple())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '_'
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IntPattern:
|
||||
value: int
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
|
||||
For an `IntPattern` this is always false
|
||||
"""
|
||||
return False
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> IntPattern(2).match(Int(1)) is None
|
||||
True
|
||||
|
||||
>>> IntPattern(1).match(Int(1))
|
||||
Some(())
|
||||
"""
|
||||
match e:
|
||||
case Int(v) if v == self.value:
|
||||
return Some(tuple())
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self.value)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SPattern:
|
||||
pred: Pattern
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
"""
|
||||
return self.pred.binds(var)
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> SPattern(NamePattern('n')).match(Int(1))
|
||||
Some((('n', 0),))
|
||||
|
||||
>>> SPattern(NamePattern('n')).match(Int(0)) is None
|
||||
True
|
||||
|
||||
>>> SPattern(SPattern(NamePattern('n'))).match(Int(4))
|
||||
Some((('n', 2),))
|
||||
"""
|
||||
match e:
|
||||
case Int(v) if v > 0:
|
||||
return self.pred.match(Int(v - 1))
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'S ' + repr(self.pred)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Builtin:
|
||||
name: str
|
||||
f: Callable[[Expression], Option[Expression]]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def try_apply(self, v: Expression) -> Option[Expression]:
|
||||
return self.f(v)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "'" + repr(self.name)[1:-1] + "'"
|
||||
|
||||
@cur2
|
||||
@staticmethod
|
||||
def _PLUS_CONST(i: int, e: Expression) -> Option[Expression]:
|
||||
match e:
|
||||
case Int(v):
|
||||
return Some(Int(i + v))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _PLUS(e: Expression) -> Option[Expression]:
|
||||
match e:
|
||||
case Int(v):
|
||||
return Some(Builtin(f'+{v}', Builtin._PLUS_CONST(v)))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def PLUS() -> 'Builtin':
|
||||
return Builtin('+', Builtin._PLUS)
|
||||
|
||||
@staticmethod
|
||||
def S() -> 'Builtin':
|
||||
return Builtin('S', Builtin._PLUS_CONST(1))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Function:
|
||||
forms: Sequence[Tuple[Pattern, Expression]]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return Function([
|
||||
(p, e if p.binds(variable) else e.subst(expression, variable))
|
||||
for (p, e) in self.forms
|
||||
])
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def try_apply(self, v: Expression) -> Option[Expression]:
|
||||
match tuple((bindings.val, body) for (pattern, body) in self.forms for bindings in (pattern.match(v),) if bindings is not None):
|
||||
case []:
|
||||
return None
|
||||
case [(bindings, body), *rest]:
|
||||
return Some(subst_all(bindings, body.subst(self, 'recur')))
|
||||
raise Exception('Unreachable')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '{ ' + ', '.join('"' + repr(repr(p))[1:-1] + '" : ' + repr(e) for (p, e) in self.forms) + ' }'
|
||||
|
||||
@dataclass
|
||||
class Application:
|
||||
expressions: Sequence[Expression]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return Application([
|
||||
e.subst(expression, variable)
|
||||
for e in self.expressions
|
||||
])
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return not len(self.expressions)
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
match self.expressions:
|
||||
case []:
|
||||
return None
|
||||
case [e]:
|
||||
return Some(e)
|
||||
case [f, a, *rest]:
|
||||
if f.is_value():
|
||||
if a.is_value():
|
||||
if isinstance(f, Function) or isinstance(f, Builtin):
|
||||
return map_opt(
|
||||
lambda maybe_f_sub: Application([maybe_f_sub, *rest]),
|
||||
f.try_apply(a)
|
||||
)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return map_opt(
|
||||
lambda next_a: Application([f, next_a, *rest]),
|
||||
a.step()
|
||||
)
|
||||
else:
|
||||
return map_opt(
|
||||
lambda next_f: Application([next_f, a, *rest]),
|
||||
f.step()
|
||||
)
|
||||
raise Exception('Unreachable')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '[ ' + ', '.join(repr(e) for e in self.expressions) + ' ]'
|
||||
|
||||
@dataclass
|
||||
class Int:
|
||||
value: int
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@dataclass
|
||||
class Variable:
|
||||
name: str
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
if variable == self.name:
|
||||
return expression
|
||||
else:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return False
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
match self.name:
|
||||
case '+':
|
||||
return Some(Builtin.PLUS())
|
||||
case 'S':
|
||||
return Some(Builtin.S())
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '"' + repr(self.name)[1:-1] + '"'
|
||||
|
||||
def subst_all(bindings: Sequence[Tuple[str, Expression]], body: Expression) -> Expression:
|
||||
match bindings:
|
||||
case []:
|
||||
return body
|
||||
case [(var, replacement), *rest]:
|
||||
return subst_all(rest, body.subst(replacement, var))
|
||||
raise Exception('Unreachable')
|
8
main.py
Normal file
8
main.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from emis_funky_funktions import *
|
||||
|
||||
from genir import json_to_ir
|
||||
from silly_thing import evaluate
|
||||
|
||||
import json, sys
|
||||
|
||||
print(evaluate(json_to_ir(json.loads(open(sys.argv[1]).read()))))
|
56
pattern.py
Normal file
56
pattern.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
from emis_funky_funktions import *
|
||||
from typing import Collection, Mapping, Sequence, Tuple, TypeAlias
|
||||
|
||||
from comb_parse import Parser
|
||||
from ir import Pattern, NamePattern, IgnorePattern, IntPattern, SPattern
|
||||
from lex import Lexeme, tokenize
|
||||
|
||||
from enum import auto, IntEnum
|
||||
import re
|
||||
|
||||
class PatTok(IntEnum):
|
||||
"""
|
||||
All possible tokens used in the grammar
|
||||
"""
|
||||
Whitespace = auto()
|
||||
Number = auto()
|
||||
Succ = auto()
|
||||
Underscore = auto()
|
||||
Name = auto()
|
||||
Eof = auto()
|
||||
|
||||
def __repr__(self):
|
||||
return self._name_
|
||||
|
||||
PATTERN_LEX_TABLE: Collection[Tuple[re.Pattern[str], PatTok]] = [
|
||||
(re.compile(r"\s+"), PatTok.Whitespace),
|
||||
(re.compile(r"\d+"), PatTok.Number),
|
||||
(re.compile(r"S"), PatTok.Succ),
|
||||
(re.compile(r"_"), PatTok.Underscore),
|
||||
(re.compile(r"\w+"), PatTok.Name),
|
||||
]
|
||||
|
||||
# P := int
|
||||
# P := name
|
||||
# P := underscore
|
||||
# P := S <P>
|
||||
|
||||
parse_int: Parser[Pattern, PatTok] = Parser.token(PatTok.Number).map(Lexeme.get_match).map(int).map(IntPattern)
|
||||
parse_name: Parser[Pattern, PatTok] = Parser.token(PatTok.Name).map(Lexeme.get_match).map(p(NamePattern))
|
||||
parse_ignore: Parser[Pattern, PatTok] = Parser.token(PatTok.Underscore).map(lambda _: IgnorePattern())
|
||||
parse_succ: Parser[Pattern, PatTok] = Parser.token(PatTok.Succ).map(k(SPattern)).fapply(Parser.lazy(lambda: parse_P)) #type: ignore
|
||||
parse_P: Parser[Pattern, PatTok] = parse_int.or_(parse_name, parse_ignore, parse_succ)
|
||||
|
||||
parse_pattern = parse_P.seq_ignore_tok(PatTok.Eof)
|
||||
|
||||
def lex_and_parse_pattern(input: str) -> Result[Pattern, str | Mapping[Lexeme[PatTok], Collection[PatTok]]]:
|
||||
match tokenize(PATTERN_LEX_TABLE, [PatTok.Whitespace], PatTok.Eof, input):
|
||||
case Ok(lexemes):
|
||||
match parse_pattern.parse_(lexemes):
|
||||
case Ok(pattern): # Imagine having a good type system
|
||||
return Ok(pattern)
|
||||
case Err(e):
|
||||
return Err(e)
|
||||
case Err(remainder):
|
||||
return Err(remainder)
|
||||
raise Exception('Unreachable')
|
283
silly_thing.py
283
silly_thing.py
|
@ -1,290 +1,11 @@
|
|||
from emis_funky_funktions import *
|
||||
from typing import Collection, Sequence, TypeAlias
|
||||
|
||||
from ir import Expression
|
||||
|
||||
from dataclasses import dataclass
|
||||
from operator import add
|
||||
|
||||
Pattern: TypeAlias = 'NamePattern | IntPattern | SPattern'
|
||||
Expression: TypeAlias = 'Function | Application | Int | Variable | Builtin'
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NamePattern:
|
||||
"""
|
||||
A pattern which always succeeds to match, and binds a whole expression to a name
|
||||
"""
|
||||
name: str
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
"""
|
||||
return var == self.name
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> NamePattern('my_var').match(Int(1))
|
||||
Some((('my_var', 1),))
|
||||
"""
|
||||
return Some(((self.name, e),))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IgnorePattern:
|
||||
"""
|
||||
A pattern which always succeeds to match, but binds nothing
|
||||
"""
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
|
||||
For an `IgnorePattern` this is always false
|
||||
"""
|
||||
return False
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> IgnorePattern().match(Int(1))
|
||||
Some(())
|
||||
"""
|
||||
return Some(tuple())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '_'
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IntPattern:
|
||||
value: int
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
|
||||
For an `IntPattern` this is always false
|
||||
"""
|
||||
return False
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> IntPattern(2).match(Int(1)) is None
|
||||
True
|
||||
|
||||
>>> IntPattern(1).match(Int(1))
|
||||
Some(())
|
||||
"""
|
||||
match e:
|
||||
case Int(v) if v == self.value:
|
||||
return Some(tuple())
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self.value)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SPattern:
|
||||
pred: Pattern
|
||||
|
||||
def binds(self, var: str) -> bool:
|
||||
"""
|
||||
Test to see if this pattern binds a given variable
|
||||
"""
|
||||
return self.pred.binds(var)
|
||||
|
||||
def match(self, e: Expression) -> Option[Sequence[Tuple[str, Expression]]]:
|
||||
"""
|
||||
Match an expression against this pattern
|
||||
|
||||
>>> SPattern(NamePattern('n')).match(Int(1))
|
||||
Some((('n', 0),))
|
||||
|
||||
>>> SPattern(NamePattern('n')).match(Int(0)) is None
|
||||
True
|
||||
|
||||
>>> SPattern(SPattern(NamePattern('n'))).match(Int(4))
|
||||
Some((('n', 2),))
|
||||
"""
|
||||
match e:
|
||||
case Int(v) if v > 0:
|
||||
return self.pred.match(Int(v - 1))
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'S ' + repr(self.pred)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Builtin:
|
||||
name: str
|
||||
f: Callable[[Expression], Option[Expression]]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def try_apply(self, v: Expression) -> Option[Expression]:
|
||||
return self.f(v)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '"' + repr(self.name)[1:-1] + '"'
|
||||
|
||||
@cur2
|
||||
@staticmethod
|
||||
def _PLUS_CONST(i: int, e: Expression) -> Option[Expression]:
|
||||
match e:
|
||||
case Int(v):
|
||||
return Some(Int(i + v))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _PLUS(e: Expression) -> Option[Expression]:
|
||||
match e:
|
||||
case Int(v):
|
||||
return Some(Builtin(f'+{v}', Builtin._PLUS_CONST(v)))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def PLUS() -> 'Builtin':
|
||||
return Builtin('+', Builtin._PLUS)
|
||||
|
||||
@staticmethod
|
||||
def S() -> 'Builtin':
|
||||
return Builtin('S', Builtin._PLUS_CONST(1))
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Function:
|
||||
forms: Sequence[Tuple[Pattern, Expression]]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return Function([
|
||||
(p, e if p.binds(variable) else e.subst(expression, variable))
|
||||
for (p, e) in self.forms
|
||||
])
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def try_apply(self, v: Expression) -> Option[Expression]:
|
||||
match self.forms:
|
||||
case []:
|
||||
return None
|
||||
case [(pattern, body), *rest]:
|
||||
match pattern.match(v):
|
||||
case Some(bindings):
|
||||
return Some(subst_all(bindings, body.subst(self, 'recur')))
|
||||
case None:
|
||||
return Function(rest).try_apply(v)
|
||||
raise Exception('Unreachable')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '{ ' + ', '.join('"' + repr(repr(p))[1:-1] + '" : ' + repr(e) for (p, e) in self.forms) + ' }'
|
||||
|
||||
@dataclass
|
||||
class Application:
|
||||
expressions: Sequence[Expression]
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return Application([
|
||||
e.subst(expression, variable)
|
||||
for e in self.expressions
|
||||
])
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return not len(self.expressions)
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
match self.expressions:
|
||||
case []:
|
||||
return None
|
||||
case [e]:
|
||||
return Some(e)
|
||||
case [f, a, *rest]:
|
||||
if f.is_value():
|
||||
if a.is_value():
|
||||
if isinstance(f, Function) or isinstance(f, Builtin):
|
||||
return map_opt(
|
||||
lambda maybe_f_sub: Application([maybe_f_sub, *rest]),
|
||||
f.try_apply(a)
|
||||
)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return map_opt(
|
||||
lambda next_a: Application([f, next_a, *rest]),
|
||||
a.step()
|
||||
)
|
||||
else:
|
||||
return map_opt(
|
||||
lambda next_f: Application([next_f, a, *rest]),
|
||||
f.step()
|
||||
)
|
||||
raise Exception('Unreachable')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '[ ' + ', '.join(repr(e) for e in self.expressions) + ' ]'
|
||||
|
||||
@dataclass
|
||||
class Int:
|
||||
value: int
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return True
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@dataclass
|
||||
class Variable:
|
||||
name: str
|
||||
|
||||
def subst(self, expression: Expression, variable: str) -> Expression:
|
||||
if variable == self.name:
|
||||
return expression
|
||||
else:
|
||||
return self
|
||||
|
||||
def is_value(self) -> bool:
|
||||
return False
|
||||
|
||||
def step(self) -> Option[Expression]:
|
||||
match self.name:
|
||||
case '+':
|
||||
return Some(Builtin.PLUS())
|
||||
case 'S':
|
||||
return Some(Builtin.S())
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return '"' + repr(self.name)[1:-1] + '"'
|
||||
|
||||
def subst_all(bindings: Sequence[Tuple[str, Expression]], body: Expression) -> Expression:
|
||||
match bindings:
|
||||
case []:
|
||||
return body
|
||||
case [(var, replacement), *rest]:
|
||||
return subst_all(rest, body.subst(replacement, var))
|
||||
raise Exception('Unreachable')
|
||||
|
||||
def evaluate(expr: Expression) -> Expression:
|
||||
"""
|
||||
>>> funktion = Function((
|
||||
|
|
Loading…
Reference in a new issue