LoginSignup
213
52

More than 3 years have passed since last update.

湯婆婆で湯婆婆を実装してみる

Last updated at Posted at 2020-11-22

はじめに

ちょっと旬を過ぎてしまったような気はしますが,どうしてもやりたかったのでやります。
やりたいことは簡単。「湯」と「婆」だけで湯婆婆を実装するだけです!!

とはいっても,そんなことをできる言語は(恐らく)存在しないため,湯婆婆言語を作成しました。

湯婆婆言語

仕様

\nのみで構成されるIMP(Instruction Modification Parameter)とコマンドでスタックを操作します。コマンドの組み合わせと意味は以下の通りです。

IMP コマンド 引数 意味
値をスタックにプッシュする
\n湯 - スタックの一番上を複製する
\n婆 - スタックの上2つをスワップする
\n\n - スタックの一番上を捨てる
婆湯 湯湯 - 加算 (スタックの上2つの和をプッシュする)
婆湯 湯婆 - 減算
婆湯 湯\n - 乗算
婆湯 婆湯 - 除算
婆湯 婆婆 - 剰余
婆湯 婆\n - スタックの一番上の値未満の非負整数の乱数をプッシュする
\n 湯湯 ラベル ラベルの設定
\n 湯婆 ラベル サブルーチンを呼び出す
\n 湯\n ラベル 指定した位置に移動
\n 婆湯 ラベル スタックの一番上がゼロのとき指定した位置に移動
\n 婆婆 ラベル スタックの一番上が負のとき指定した位置に移動
\n 婆\n - サブルーチンを終了して戻る
\n \n\n - プログラムを終了する
婆\n 湯湯 - スタックの一番上の文字を出力する
婆\n 湯婆 - スタックの一番上の数値を出力する
婆\n 婆湯 - 1文字を読み取ってスタックにプッシュする
婆\n 婆婆 - 数値を読み取ってスタックにプッシュする

数値は湯が0,婆が1に対応する2進数で表現し,\nで終了します。符号が付く場合は湯が正,婆が負を表します。

……なんだか見たことのある言語ですって? そうです,Whitespaceのスペースとタブを湯と婆に置き換えた言語とほぼ一致1します。

実装

湯婆婆インタプリタをPython 3で実装しました。dequeをスタックとして使用し,上記の仕様に基づいてスタック操作とフロー制御を行うようにしました。

2020/11/23追記: ソースコード
aburaya.py
import click
import os
import sys
from collections import deque
from random import randint
from re import Scanner

CHAR_MAP = {'yu': '湯', 'ba': '婆', 'lf': chr(10)}

IMP_CONST = [
    (CHAR_MAP['yu'], 'STACK_MANIPULATION'),
    (CHAR_MAP['ba'] + CHAR_MAP['yu'], 'ARITHMETIC'),
    (CHAR_MAP['lf'], 'FLOW_CONTROL'),
    (CHAR_MAP['ba'] + CHAR_MAP['lf'], 'IO')
]

STACK_MANIPULATION_CONST = [
    (CHAR_MAP['yu'], 'PUSH'),
    (CHAR_MAP['lf'] + CHAR_MAP['yu'], 'DUP'),
    (CHAR_MAP['lf'] + CHAR_MAP['ba'], 'SWAP'),
    (CHAR_MAP['lf'] + CHAR_MAP['lf'], 'POP')
]

ARITHMETIC_CONST = [
    (CHAR_MAP['yu'] * 2, '+'),
    (CHAR_MAP['yu'] + CHAR_MAP['ba'], '-'),
    (CHAR_MAP['yu'] + CHAR_MAP['lf'], '*'),
    (CHAR_MAP['ba'] + CHAR_MAP['yu'], '/'),
    (CHAR_MAP['ba'] * 2, '%'),
    (CHAR_MAP['ba'] + CHAR_MAP['lf'], 'RND')
]

FLOW_CONTROL_CONST = [
    (CHAR_MAP['yu'] * 2, 'MARK'),
    (CHAR_MAP['yu'] + CHAR_MAP['ba'], 'CALL'),
    (CHAR_MAP['yu'] + CHAR_MAP['lf'], 'JUMP'),
    (CHAR_MAP['ba'] + CHAR_MAP['yu'], 'JUMP_IF_ZERO'),
    (CHAR_MAP['ba'] * 2, 'JUMP_IF_NEG'),
    (CHAR_MAP['ba'] + CHAR_MAP['lf'], 'END_SUB'),
    (CHAR_MAP['lf'] * 2, 'END')
]

IO_CONST = [
    (CHAR_MAP['yu'] * 2, 'OUTPUT_CHAR'),
    (CHAR_MAP['yu'] + CHAR_MAP['ba'], 'OUTPUT_NUM'),
    (CHAR_MAP['ba'] + CHAR_MAP['yu'], 'READ_CHAR'),
    (CHAR_MAP['ba'] * 2, 'READ_NUM')
]

NUM_CONST = {CHAR_MAP['yu']: "0", CHAR_MAP['ba']: "1"}

NUM_SIGN_CONST = {CHAR_MAP['yu']: 'POSITIVE', CHAR_MAP['ba']: 'NEGATIVE'}

HAS_ARGS = ['PUSH', 'MARK', 'CALL', 'JUMP', 'JUMP_IF_ZERO', 'JUMP_IF_NEG']


class YTokenizer():
    def __init__(self, type=None, value=None):
        self.value = value
        self.type = type

    def __str__(self):
        return f'Token({self.get_type()}, {self.get_value()})'

    def __repr__(self):
        return self.__str__()

    def get_type(self):
        return self.type

    def get_value(self):
        value = self.value
        return value

    def _scan_int(self, string, const):
        patterns = []
        INT_SIGN = (r"^[{}{}]".format(CHAR_MAP['yu'], CHAR_MAP['ba']),
                    lambda scanner, token: ("INT_SIGN", token))
        INT_VAL = (r".[{}{}]*".format(CHAR_MAP['yu'], CHAR_MAP['ba']),
                     lambda scanner, token: ("INT_VAL", token))
        if const == 'SIGNED_INT':
            patterns.append(INT_SIGN)
        patterns.append(INT_VAL)
        scanner = Scanner(patterns)
        found, remainder = scanner.scan(string)
        self.type = 'INT'
        try:
            self.value = ''.join([f[1] for f in found])
        except IndexError:
            print(f'Hit IndexError, string trying to check is: {string}')

    def _scan_command(self, line, pos, const):
        patterns = [(r"^{}".format(i[0]), i[1]) for i in const]
        scanner = Scanner(patterns)
        found, remainder = scanner.scan(line[pos:])
        self.type = found[0]
        self.value = [i[0] for i in const if i[1] == self.type][0]

    def scan(self, line, pos, const):
        if const in ['LABEL', 'SIGNED_INT']:
            self._scan_int(line[pos:], const)
        else:
            self._scan_command(line, pos, const)



class YLexer():
    def __init__(self, line):
        self.line = self._skip_others(line)
        self.pos = 0
        self.tokens = [[]]

    def _skip_others(self, line):
        return ''.join([i for i in line if i in CHAR_MAP.values()])

    def _get_const(self, token_type):
        if token_type == 'STACK_MANIPULATION':
            return STACK_MANIPULATION_CONST
        elif token_type == 'ARITHMETIC':
            return ARITHMETIC_CONST
        elif token_type == 'FLOW_CONTROL':
            return FLOW_CONTROL_CONST
        elif token_type == 'IO':
            return IO_CONST
        elif token_type == 'NUM':
            return NUM_CONST
        return None

    def _get_int(self, t):
        token = YTokenizer()
        const = 'SIGNED_INT' if t == 'PUSH' else 'LABEL'
        token.scan(self.line, self.pos, const)
        return token

    def _get_token(self, const):
        token = YTokenizer()
        token.scan(self.line, self.pos, const)
        return token

    def get_all_tokens(self):
        while self.pos < len(self.line):
            req_tokens = 2
            const = IMP_CONST if len(self.tokens[-1]) == 0 else self._get_const(self.tokens[-1][0].type)
            token = self._get_token(const)
            self.pos = self.pos + len(token.value)
            self.tokens[-1].append(token)
            if token.type in HAS_ARGS:
                self.tokens[-1].append(self._get_int(token.type))
                self.pos = self.pos + len(self.tokens[-1][-1].value) + 1
                req_tokens += 1
            if len(self.tokens[-1]) == req_tokens:
                self.tokens.append([])
        del self.tokens[-1]



class YParser():
    def __init__(self, tokens):
        self.token_list = tokens
        self.stack = deque()
        self.labels = self.create_labels()
        self.num_of_tokens = len(tokens)
        self.instruction_ptr = 0
        self.call_ptr = []
        self.method_map = {
            'STACK_MANIPULATION': {
                'PUSH': self.push,
                'DUP': self.dup,
                'SWAP': self.swap,
                'POP': self.pop
            },
            'IO': {
                'OUTPUT_CHAR': self.o_chr,
                'OUTPUT_NUM': self.o_int,
                'READ_CHAR': self.i_chr,
                'READ_NUM': self.i_int
            },
            'FLOW_CONTROL': {
                'MARK': (lambda x: None),
                'CALL': self.call_sub,
                'JUMP': self.jump_loc,
                'JUMP_IF_ZERO': self.jump_zero,
                'JUMP_IF_NEG': self.jump_neg,
                'END_SUB': self.end_sub,
                'END': self.end
            },
            'ARITHMETIC': {
                '+': self.add,
                '-': self.sub,
                '*': self.mul,
                '/': self.div,
                '%': self.mod,
                'RND': self.rnd
            }
        }

    def call_sub(self, lbl):
        self.call_ptr.append(self.instruction_ptr)
        self.instruction_ptr = self.labels[lbl]

    def jump_loc(self, lbl):
        self.instruction_ptr = self.labels[lbl]

    def jump_zero(self, lbl):
        if self.stack.pop() == 0:
            self.instruction_ptr = self.labels[lbl]
        else:
            pass

    def jump_neg(self, lbl):
        if self.stack.pop() < 0:
            self.instruction_ptr = self.labels[lbl]
        else:
            pass

    def end_sub(self):
        self.instruction_ptr = self.call_ptr.pop()

    def end(self):
        sys.exit(0)

    def parse(self):
        while self.instruction_ptr < self.num_of_tokens:
            token = self.token_list[self.instruction_ptr]
            if token[1].type in HAS_ARGS:
                signed = token[1].type == 'PUSH'
                int_value = self._get_value(token[2].value, signed=signed)
                self.method_map[token[0].type][token[1].type](int_value)
            else:
                self.method_map[token[0].type][token[1].type]()
            self.instruction_ptr += 1

    def _get_value(self, y_int, signed=False):
        if signed:
            sign = '-' if NUM_SIGN_CONST[y_int[0]] == 'NEGATIVE' else ''
            y_int = y_int[1:]
        number = int(''.join([NUM_CONST[i] for i in y_int]), 2)
        return int(f'{sign}{number}') if signed else number

    def create_labels(self):
        labels = dict(
            (
                (
                    (self._get_value(t[2].value), i) for i, t in enumerate(self.token_list)
                        if t[0].type == 'FLOW_CONTROL' and t[1].type == 'MARK'
                )
            )
        )
        return labels

    # stack
    def push(self, item):
        self.stack.append(item)

    def isempty(self):
        return len(self.stack) == 0

    def dup(self):
        if not self.isempty():
            self.stack.append(self.stack[-1])

    def swap(self):
        self.stack[-1], self.stack[-2] = self.stack[-2], self.stack[-1]

    def pop(self):
        self.stack.pop()

    # io
    def i_chr(self):
        c = sys.stdin.read(1)
        if len(c) == 0:
            self.stack.append(0)
        else:
            self.stack.append(ord(c))

    def i_int(self):
        num = None
        while type(num) is not int:
            try:
                num = int(input())
            except ValueError:
                pass
        self.stack.append(num)

    def o_chr(self):
        char = chr(self.stack.pop())
        sys.stdout.buffer.write(char.encode('utf-8'))
        sys.stdout.buffer.flush()

    def o_int(self):
        integer = self.stack.pop()
        sys.stdout.buffer.write(str(integer).encode('utf-8'))
        sys.stdout.buffer.flush()

    # math
    def _get_oeprands(self):
        right = int(self.stack.pop())
        left = int(self.stack.pop())
        return left, right

    def add(self):
        operands = self._get_oeprands()
        self.stack.append(operands[0] + operands[1])

    def sub(self):
        operands = self._get_oeprands()
        self.stack.append(operands[0] - operands[1])

    def mul(self):
        operands = self._get_oeprands()
        self.stack.append(operands[0] * operands[1])

    def div(self):
        operands = self._get_oeprands()
        self.stack.append(operands[0] // operands[1])

    def mod(self):
        operands = self._get_oeprands()
        self.stack.append(operands[0] % operands[1])

    def rnd(self):
        maximum = self.stack.pop()
        self.stack.append(randint(0, maximum-1))

@click.command()
@click.argument('filename')
@click.option('--encoding', '-e', default='utf-8')
def main(filename, encoding):
    """YuBaBa Lang Interpreter: Abura-ya"""

    assert os.path.isfile(filename)
    with open(filename, 'r', encoding=encoding) as f:
        lines = f.read()
        item = YLexer(line=lines)
        item.get_all_tokens()
        p = YParser(item.tokens)
        p.parse()

if __name__ == '__main__':
    main()

Alt湯婆婆

湯婆婆言語でプログラミングを行うことは非常に困難です。文字を1文字出力するだけでも,文字コードを調べて2進数にして湯と婆に置換する必要があります。そこで,より簡単に記述できるAlt湯婆婆を作成し,トランスパイルして湯婆婆のソースコードを生成するのが実用的でよいでしょう。文字列出力を簡略化するだけでもかなり負担が減ります。

湯婆婆 in 湯婆婆

ソースコード
湯湯湯婆湯婆婆湯湯婆湯婆湯婆湯湯湯婆
婆
湯湯湯湯湯婆婆婆婆婆湯婆湯湯湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯婆婆湯婆婆婆婆婆湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯湯婆湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯婆婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯婆湯湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆湯婆婆
婆
湯湯湯湯湯婆湯婆湯婆湯湯湯湯湯湯婆婆湯婆
婆
湯湯湯湯湯婆湯婆湯湯婆湯湯婆湯湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯婆湯湯婆湯
婆
湯湯湯湯湯婆婆湯湯婆婆湯婆婆婆婆婆湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆湯婆湯
婆
湯湯婆
婆湯湯湯湯婆婆湯湯湯湯婆婆湯婆湯婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆婆婆婆湯湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯
湯湯湯湯婆湯婆湯
婆湯湯婆
婆湯婆湯
湯
湯婆
湯湯湯湯湯婆

湯湯婆
婆
婆湯湯
湯湯湯湯婆湯婆湯
婆湯湯婆
婆湯婆湯
湯
湯婆
湯湯湯
婆湯湯湯婆
婆湯湯湯
湯
婆

湯湯婆湯
湯

婆湯婆

湯湯婆婆
湯
湯
婆湯婆湯湯
湯湯湯婆
婆湯湯婆湯
婆湯


湯
婆婆

湯湯婆湯湯
湯

湯湯湯婆婆湯湯湯湯湯婆婆湯婆湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆婆婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆湯湯湯婆婆湯婆湯湯湯湯湯婆湯婆
婆
湯湯湯湯湯婆婆湯婆婆湯湯婆湯婆湯湯湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆湯婆湯
婆
湯湯湯湯湯婆湯婆湯婆湯湯湯湯湯湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆湯婆湯
婆
湯湯湯湯湯婆湯湯婆婆婆湯婆婆湯湯婆湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯湯婆湯湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆湯婆湯
婆
湯湯湯湯湯婆湯婆湯湯婆湯湯婆湯湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆婆婆湯
婆
湯湯湯湯湯婆湯婆湯婆湯湯湯湯湯湯婆婆湯婆
婆
湯湯湯湯湯婆湯婆湯湯婆湯湯婆湯湯婆婆湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯婆婆婆婆
婆
湯湯湯
湯婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯湯婆湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯湯婆
婆
湯湯湯
湯婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯湯婆湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯婆湯
婆
湯湯湯湯湯婆湯婆湯湯婆湯湯湯湯湯湯婆婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯婆婆婆婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯湯婆湯湯婆
婆
湯湯湯湯湯婆湯湯湯婆婆婆婆婆婆湯婆湯婆湯湯
婆
湯湯湯湯湯婆湯湯婆婆婆湯婆湯湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯婆湯湯婆湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆湯婆婆湯湯婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯湯婆湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯婆湯湯婆湯湯婆婆
婆
湯湯湯湯湯婆婆湯湯湯湯湯婆婆湯湯湯湯湯
婆
湯湯湯湯湯婆婆湯湯湯湯湯湯湯湯湯湯湯婆
婆
湯湯婆
湯湯湯湯湯婆湯湯湯湯婆
婆
湯湯湯湯湯婆湯湯湯湯婆
婆
湯湯

補足

インタプリタの実装の都合上,文字の読み取り時に改行文字が入るまでは制御が戻らないことを悪用しています。
贅沢な名前であることを確認するために一度元の名前を復唱する必要がありますが,この処理がめんどくさかったので,スタックに積んでいきながら出力して,改行文字を読んだらループを終了しています。
読み取り時にはカウンタをインクリメントして文字数を数えて,その文字数を超えない乱数を生成しています。その後,乱数の分だけ名前の後ろを切り取ることでランダムな新しい名前を生成しています。

参考: Alt湯婆婆
out_str('契約書だよ。そこに名前を書きな。\n')

INPUT_START = 1
INPUT_END = 2

# 1文字目を読むときだけループの外にしないと毎回「フン。」って言われてしまう
input_char()
out_str('フン。')
dup()
push(0x0A)
sub()
if_zero(INPUT_END)
dup()
out_char()

# 2文字目以降の読み取り
push(1)
mark(INPUT_START)
input_char()
dup()
push(0x0A)
sub()
if_zero(INPUT_END)
dup()
out_char()

swap()
push(1)
add()
jump(INPUT_START)

mark(INPUT_END)
pop()

rnd()

START = 3
END = 4

# 乱数をデクリメントしながら名前を削っていく
mark(START)
dup()
if_zero(END)
push(1)
sub()
swap()
pop()
jump(START)
mark(END)

pop()
out_str('というのかい。贅沢な名だねぇ。\n今からお前の名前は')
dup()
out_char()
out_str('だ。いいかい、')
dup()
out_char()
out_str('だよ。分かったら返事をするんだ、')
out_char()
out_str('!!')

実行例

荻野千尋

$ python aburaya.py yubaba.ybb
契約書だよ。そこに名前を書きな。
荻野千尋
フン。荻野千尋というのかい。贅沢な名だねぇ。
今からお前の名前は千だ。いいかい、千だよ。分かったら返事をするんだ、千!!

いい感じですね

𠮷田さん

$ python aburaya.py yubaba.ybb
契約書だよ。そこに名前を書きな。
𠮷田
フン。𠮷田というのかい。贅沢な名だねぇ。
今からお前の名前は𠮷だ。いいかい、𠮷だよ。分かったら返事をするんだ、𠮷!!

Windowsのコマンドプロンプトの問題により画面上では表示されませんが,しかるべき場所にコピペすれば正しく処理されていることを確認できます。

(空文字列)

$ python aburaya.py yubaba.ybb
契約書だよ。そこに名前を書きな。

フン。Traceback (most recent call last):
  (中略)
IndexError: pop from an empty deque

仕様通りクラッシュします (ユーザー名等が含まれるためスタックトレースは省略します)

さいごに

もうちょっと長くなるかと思ったんですが意外と少ないですね(189行)。
非常に下らないとは思いますが,笑っていただければ幸いです。
最後まで読んでいただきありがとうございました。


  1. 細かいことを言うと多少異なります。Whitespace言語(WSL)だけだと乱数を生成できなかったので,新たなコマンドを追加しました。また,WSLではヒープが存在しますが,めんどくさかったので省略しました。そのため,標準入力からの読み取りで,WSLではスタックの一番上で指定される位置に読み取りとなっていますが,湯婆婆ではスタックの一番上に載せるようにしています。従って,既にWSLで実装されている湯婆婆のソースコードを単純に置換しただけでは動作しないので注意が必要です。 

213
52
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
213
52