Pegjs: Berikan cara ringkas untuk menunjukkan nilai pengembalian tunggal dari suatu urutan

Dibuat pada 29 Jan 2014  ·  21Komentar  ·  Sumber: pegjs/pegjs

Ini cukup umum bagi saya. Saya ingin mengembalikan hanya bagian yang relevan dari suatu urutan di label init. Dalam hal ini, hanya AssignmentExpression.

pattern:Pattern init:(_ "=" _ a:AssignmentExpression {return a})?

Saya sarankan Anda menambahkan @ ekspresi yang hanya akan mengembalikan ekspresi berikut dari urutan. Jadi contoh di atas akan terlihat seperti ini:

pattern:Pattern init:(_ "=" _ @AssignmentExpression)?

Masalah Terkait:

  • #427 Izinkan pengembalian hasil kecocokan dari ekspresi tertentu dalam aturan tanpa tindakan
  • #545 Ekstensi sintaks sederhana untuk mempersingkat tata bahasa
feature

Komentar yang paling membantu

Apakah ini sudah dirilis ke tag dev di npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Semua 21 komentar

Saya setuju ini adalah masalah umum. Tapi saya tidak yakin apakah itu layak untuk dipecahkan. Dengan kata lain, saya tidak yakin apakah itu cukup sering terjadi dan apakah itu menyebabkan rasa sakit yang cukup untuk menjamin penambahan kompleksitas ke PEG.js dengan mengimplementasikan solusi, apa pun itu.

Saya akan membiarkan masalah ini terbuka dan menandainya untuk dipertimbangkan setelah 1.0.0.

Sepakat. Saya sangat ingin melihat ini di 1.0. Saya tidak begitu antusias dengan sintaks @. IMHO ide yang lebih baik adalah melakukan ini: jika hanya ada satu label "tingkat atas" maka secara implisit kembalikan label itu. Jadi alih-alih:

rule = space* a:(text space+ otherText)+ newLine* { return a; }

Anda mendapatkan:

rule = space* a:(text space+ otherText)+ newLine*

Dan ketika labelnya tidak berarti apa-apa, izinkan juga ini:

rule = space* :(text space+ otherText)+ newLine*

Jadi lewati nama label sama sekali.

@mulderr Saya pikir operator @ lebih baik, karena melakukan sesuatu secara implisit berarti bahwa jika saya membaca kode orang lain yang menggunakan fitur itu, saya harus melalui banyak googling sebelum saya menyadari apa yang dia lakukan di sana. Sebaliknya, menggunakan operator eksplisit akan memungkinkan saya untuk mencari dokumentasi dengan cepat.

+1 untuk @ - Ini sering terjadi di seluruh kode saya.

+1 untuk beberapa cara ringkas untuk melakukan ini.

Sepertinya rasa sakit ini mirip dengan sintaks verbose function di JS, yang dialamatkan ES6 menggunakan arrow functions . Mungkin sesuatu yang serupa dapat digunakan di sini? Sesuatu seperti:

rule = (space* a:(text space+ otherText) newLine*) => a

Menurut saya ini cukup fleksibel, masih eksplisit (perhatian ala @wildeyes ), dan terasa kurang seperti menambahkan kompleksitas karena dalam sintaks dan implementasinya mengacu pada JS yang mendasarinya ...

Saya telah membayangkan sesuatu seperti:

additive = left:multiplicative "+" right:additive {= left + right; }

Di mana = (jangan ragu untuk memperdebatkan pilihan karakter) sebagai karakter non-spasi pertama dari sebuah blok diubah menjadi return .

Ini juga akan berfungsi untuk ekspresi penuh dan harus dimungkinkan dengan pass transformasi.

Ada berita? Mengapa tidak additive = left:multiplicative "+" right:additive { => left + right } ?

Itu pasti akan terasa intuitif mengingat bagaimana fungsi panah sekarang bekerja ( (left, right) => left + right ).

Sebenarnya ada beberapa contoh tempat di file parser.pegjs Anda yang akan ditingkatkan dengan fitur ini.

Contohnya:

  = head:ActionExpression tail:(__ "/" __ ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: buildList(head, tail, 3),
            location: location()
          }
        : head;
    }

Rapuh karena nomor ajaib 3 dalam panggilan buildList yang secara non-intuitif terkait dengan posisi ActionExpression Anda dalam urutan. Fungsi buildList sendiri rumit dengan menggabungkan dua operasi yang berbeda. Dengan menggunakan ekspresi @ dan sintaks spread es6, ini menjadi lebih bersih:

  = head:ActionExpression tail:(__ "/" __ @ActionExpression)* {
      return tail.length > 0
        ? {
            type: "choice",
            alternatives: [head, ...tail],
            location: location()
          }
        : head;
    }

Karena ini hanyalah gula sintaksis, saya dapat menambahkan fitur ini ke parser Anda hanya dengan memodifikasi ActionExpression di parser.pegjs

  = ExtractSequenceExpression
  / expression:SequenceExpression code:(__ CodeBlock)? {
      return code !== null
        ? {
            type: "action",
            expression: expression,
            code: code[1],
            location: location()
          }
        : expression;
    }

ExtractExpression
  = "@" __ expression:PrefixedExpression {
      return {
        type: "labeled",
        label: "value",
        expression: expression,
        location: location()
      };
    }

ExtractSequenceExpression
  = head:(__ PrefixedExpression)* _ extract:ExtractExpression tail:(__ PrefixedExpression)* {
      return {
        type: "action",
        expression: {
          type: "sequence",
          elements: extractList(head, 1).concat(extract, extractList(tail, 1)),
          location: location()
        },
        code: "return value;",
        location: location()
      }
    }

Saya memeriksa inti yang menunjukkan bagaimana parser.pegjs akan disederhanakan dengan menggunakan notasi @.

Fungsi extractOptional, extractList, dan buildList telah sepenuhnya dihapus, karena notasi @ membuatnya sepele untuk mengekstrak nilai yang diinginkan dari suatu urutan.

https://Gist.github.com/krisnye/a6c2aac94ffc0e222754c52d69e44b83

@krisnye Begini tampilannya jika ada lebih banyak sintaks gula:

https://github.com/polkovnikov-ph/newpeg/blob/master/parse.np

Saya berpikir untuk menggunakan kombinasi :: dan # untuk fitur sintaks ini (lihat penjelasan/alasan saya di #545 ):

  • :: operator pengikat
  • # operator ekspansi
// this is imported into grammar
class List extends Array {
  constructor() { this.isList = true; }
}

// grammar
number = _ ::value+ _
value = ::int #(_ "," _ ::int)* { return new List(); }
int = $[0-9]+
_ = [ \t]*
  • Pada urutan root aturan, :: akan mengembalikan hasil ekspresi sebagai hasil aturan
  • Dalam urutan bersarang, :: akan mengembalikan hasil ekspresi bersarang sebagai hasil urutan
  • Jika lebih dari satu :: digunakan, hasil dari ekspresi yang ditandai akan dikembalikan sebagai array
  • Jika # digunakan pada urutan bersarang yang berisi :: , hasilnya akan dimasukkan ke dalam larik induk
  • Jika aturan memiliki blok kode bersama dengan :: / # , maka jalankan terlebih dahulu, lalu gunakan metode hasil push

Mengikuti aturan ini, meneruskan 09 , 55, 7 ke parser yang dihasilkan dari contoh di atas akan menghasilkan:

result = [
    isList: true
    0: "09"
    1: "55"
    2: "7"
]

result instanceof Array # true in ES2015+ enviroments
result instanceof List # true

Pada urutan root aturan, :: akan mengembalikan hasil ekspresi sebagai hasil aturan
Dalam urutan bersarang, :: akan mengembalikan hasil ekspresi bersarang sebagai hasil urutan

Mengapa ini terpisah? Mengapa tidak " :: pada suatu urutan membuat hasil argumen hasil dari urutan"?

Jika lebih dari satu :: digunakan, hasil dari ekspresi yang ditandai akan dikembalikan sebagai array.

Itu ide yang buruk. Ini lebih suka bail out dalam kasus ini. Tidak ada gunanya menggabungkan nilai dari beberapa jenis ke dalam array. (Itu akan menjadi Tuple, tetapi mereka tidak berguna di JS.)

Jika # digunakan pada urutan, hasilnya akan menjadi Array#concat'ed ke dalam array induknya

Sekarang itu ide yang mengerikan. Ini jelas merupakan kludge untuk menyingkirkan { return xs.push(x), xs } asing. Membuat kasus khusus seperti itu tidak masuk akal, karena dapat diselesaikan dengan cara umum dengan aturan parameter. Tidak banyak urutan karakter tunggal yang digunakan untuk operator, dan kita tidak boleh menyia-nyiakannya.

Jika aturan memiliki blok kode bersama dengan ::/#, maka jalankan terlebih dahulu, lalu gunakan metode push hasil

Jadi f = ::"a" { return "b" } seharusnya menghasilkan ["a", "b"] ?

melewati 09 . 55: 7 ke parser yang dihasilkan contoh di atas akan menghasilkan:

Tidak akan, tidak ada . atau : disebutkan. Saya juga tidak melihat bagaimana perilaku yang dijelaskan akan menghasilkan hasilnya.

bilangan = _ ::nilai+ _

Juga bukan itu cara _ seharusnya digunakan. Itu pergi ke kanan atau ke kiri token, sisi dipilih sekali untuk tata bahasa. Jika ke kanan, aturan main juga harus dimulai dengan _ dan sebaliknya.

start = _ value
value = ::int #("," _ ::int)* { return new List(); }
number = ::$[0-9]+ _
_ = [ \t]*

Contoh kelas yang diimplementasikan dalam contoh JavaScript (tidak dimaksudkan sebagai implementasi nyata):

ClassMethod
  = head:FunctionHead __ params:FunctionParameters __ body:FunctionBody {
      return {
        type: "method",
        name: head[ 1 ],
        modifiers: head[ 0 ],
        params: params,
        body: body
      };
    }

// `::` inside the zero_or_more "( ... )*" builds an array as we want,
// so this rule returns `[FunctionModifier[], Identifier]` as expected
MethodHead = (::MethodModifier __)* ("function" __)? ::Identifier

// https://github.com/tc39/proposal-class-fields#private-fields
MethodModifier
  = "#"
  / "static"
  / "async"

FunctionParameters
  = "(" __ head:FunctionParam tail:(__ "," __ ::FunctionParam)* __ ")" {
      // due to `::`, tail is `FunctionParam[]` instead of `[__, "", __, FunctionParam][]`
      return [ head ].concat( tail );
    }
    / "(" __ ")" { return []; }

FunctionParam
  = name:Identifier value:(__ "=" __ ::Expression)? {
      return { name, value };
    }

FunctionBody = "{" __ ::SourceElements? __ "}"

Mengapa ini terpisah? Mengapa tidak :: pada suatu urutan membuat hasil argumen hasil dari urutan"?

Bukankah itu hal yang sama? Saya hanya membuatnya lebih jelas sehingga hasil dari kasus penggunaan yang berbeda seperti MethodHead , FunctionParam dan FunctionBody dapat dengan mudah dipahami.

Tidak ada gunanya menggabungkan nilai dari beberapa jenis ke dalam array. (Itu akan menjadi Tuple, tetapi mereka tidak berguna di JS.)

Saat membangun AST (hasil paling umum dari parser yang dihasilkan), menurut saya, itu akan menyederhanakan kasus penggunaan seperti MethodHead , alih-alih menulis:

MethodHead
  = modifiers:(::MethodModifier __)* ("function" __)? name:Identifier {
      return [ modifiers, name ];
    }

Setelah memikirkannya lebih lanjut, meskipun menyederhanakan kasus penggunaan, itu juga membuka kemungkinan pengembang membuat kesalahan (baik dalam cara mereka menerapkan tata bahasa mereka, atau bagaimana hasilnya ditangani oleh tindakan), oleh karena itu saya pikir menempatkan ini use case di belakang opsi seperti multipleSingleReturns (default: false ) akan menjadi tindakan terbaik di sini (Jika fitur ini diterapkan).

Jika # digunakan pada urutan, hasilnya akan menjadi Array#concat'ed ke dalam array induknya

Sekarang itu ide yang mengerikan. Ini jelas merupakan kludge untuk menyingkirkan yang asing { return xs.Push(x), xs }

Ini juga membantu dalam kasus penggunaan yang lebih umum seperti FunctionParameters mana akan lebih baik untuk menulis:

// should always return `FunctionParam[]`
FunctionParameters
  = "(" __ ::FunctionParam #(__ "," __ ::FunctionParam)* __ ")"
  / "(" __ ")" { return []; }

itu bisa diselesaikan dengan cara umum dengan aturan parameter

Saya berpikir bahwa saya harus menerapkan nilai pengembalian tunggal sebelum aturan parameter karena saya masih tidak yakin bagaimana melanjutkannya nanti (saya suka menggunakan templat, tetapi menggunakan rule < .., .. > = .. tampaknya menambah banyak gangguan pada tata bahasa PEG.js, jadi saya mencoba memikirkan sedikit perubahan sintaks untuk membuatnya lebih cocok), tapi itu masalah terpisah.

Tidak banyak urutan karakter tunggal yang digunakan untuk operator, dan kita tidak boleh menyia-nyiakannya.

Itu benar, tetapi jika kita tidak menggunakannya saat acara itu sendiri seperti ini, maka itu sama buruknya.

Saya berpikir untuk menggunakan # untuk kasus penggunaan ini setelah mengingat bahwa ini digunakan sebagai operator ekspansi dalam beberapa bahasa yang mengimplementasikan arahan preprocess, dan pada dasarnya itulah yang dicakup oleh kasus penggunaan ini di sini.

Jadi f = ::"a" { return "b" } seharusnya memiliki ["a", "b"] sebagai hasilnya?

Tidak, karena blok kode diharapkan oleh parser untuk mengembalikan objek seperti array yang berisi metode push (jadi f = ::"a" { return [ "b" ] } mengembalikan [ "b", "a" ] sebagai hasilnya), oleh karena itu memungkinkan untuk mendorong tidak hanya ke dalam array, tetapi node khusus yang memiliki metode yang sama diimplementasikan untuk bekerja dengan cara yang sama. Melihat bagaimana pemikiran pertama Anda setelah membacanya, apakah akan lebih baik untuk memahami jika ini ada di balik opsi seperti pushSingleReturns ? Jika opsi ini adalah false (default), memiliki blok kode setelah urutan yang berisi nilai pengembalian tunggal akan menimbulkan kesalahan.

melewati 09 . 55: 7 ke parser yang dihasilkan contoh di atas akan menghasilkan:

Tidak, tidak ada . atau : disebutkan.

Maaf, itu adalah kesalahan yang tertinggal ketika saya menulis ulang contoh yang diberikan.

Juga bukan itu cara _ seharusnya digunakan. Itu pergi ke kanan atau ke kiri token, sisi dipilih sekali untuk tata bahasa. Jika ke kanan, aturan utama juga harus dimulai dengan _ dan sebaliknya.

Saya pikir ini hanya masalah preferensi saja :smile:, meskipun saya pikir akan lebih mudah untuk memahami contoh ini:

number = _ ::value+ EOS
...
EOS = !.

Saya juga tidak melihat bagaimana perilaku yang dijelaskan akan menghasilkan hasilnya.

Apakah komentar yang diperbarui ini membantu untuk memahami apa yang saya coba katakan, dan jika tidak, apa yang tidak Anda mengerti.

Saya setuju dengan @polkovnikov-ph bahwa perubahan yang ditawarkan oleh tempat sangat tidak jelas dan hanya akan menjadi sumber kesalahan tambahan.

Jika # digunakan pada urutan, hasilnya akan menjadi Array#concat'ed ke dalam array induknya

Apa yang harus dikembalikan dalam tata bahasa start = #('a') ? Sejauh yang saya mengerti, dia berpikir bagaimana gula sintaks untuk meratakan array? Saya tidak berpikir bahwa operator ini diperlukan. Untuk penggunaannya yang paling jelas -- ekspresi daftar anggota dengan pemisah -- sintaks khusus lebih baik dibuat (lihat #30 dan garpu saya, https://github.com/Mingun/pegjs/commit/db4b2b102982a53dbed1f579477c85c06f8b92e6).

Jika aturan memiliki blok kode bersama dengan ::/#, maka jalankan terlebih dahulu, lalu gunakan metode push hasil

Perilaku yang sangat tidak jelas. Tindakan dalam sumber tata bahasa terletak setelah ekspresi dan biasanya dipanggil setelah penguraian ekspresi. Dan tiba-tiba entah bagaimana mereka mulai dipanggil sebelum mengurai. Bagaimana label berperilaku?

Sisa 3 poin yang berhasil Anda gambarkan sehingga akal sehat mereka mulai keluar. Seberapa benar catatan @polkovnikov-ph mengapa dalam deskripsi untuk memisahkan kasus pertama dan kedua? Hanya dua aturan sederhana yang akan dieksekusi:

  1. :: (terus terang, saya tidak suka pilihan karakter ini, terlalu berisik) sebelum ekspresi mengarah pada fakta bahwa dari elemen simpul sequence ditandai dengan karakter ini kembali
  2. Jika dalam urutan hanya satu elemen seperti itu, hasilnya dikembalikan, jika tidak, array hasil akan dikembalikan

Contoh:

start =   'a' 'b'   'c'; // => ['a', 'b', 'c']
start = ::'a' 'b'   'c'; // => 'a'
start = ::'a' 'b' ::'c'; // => ['a', 'c']

Contoh besar menjelaskan dengan tepat apa yang saya harapkan dari :: dan tidak menjelaskan kasus penggunaan yang meragukan ( # , beberapa :: ).

Bukankah itu hal yang sama?

Itu yang saya tanyakan. Deskripsi dengan cara umum biasanya lebih berguna, karena membuat pembaca yakin bahwa itu benar-benar hal yang sama. Terima kasih atas klarifikasinya. :)

menurut saya, itu akan menyederhanakan kasus penggunaan seperti MethodHead

Tapi mengapa tidak membuat objek saja? Ada modifiers: dan name: dalam notasi, biarkan seperti pada objek JS yang dihasilkan, dan itu akan menjadi keren.

itu juga membuka kemungkinan pengembang melakukan kesalahan (baik dalam cara mereka menerapkan tata bahasa mereka, atau bagaimana hasilnya ditangani oleh tindakan)

Awalnya saya akan menulis tentang ini, tetapi kemudian memutuskan bahwa saya tidak memiliki argumen yang cukup kuat. Saya lebih suka tidak mengizinkan beberapa :: pada tingkat urutan yang sama sama sekali (bahkan dengan bendera).

Ini juga membantu dalam kasus penggunaan yang lebih umum seperti FunctionParameters di mana akan lebih baik untuk menulis:

Tapi itu hal yang sama. Sintaks yang sangat bagus adalah inter(FunctionParam, "," __) , dengan

inter a b = x:a xs:(b ::a)* { return xs.unshift(x), xs; }

aturan < .., .. > = .. tampaknya menambahkan banyak gangguan pada tata bahasa PEG.js

Orang mengharapkan <...> digunakan untuk tipe, sedangkan dalam kasus ini argumen bukan tipe. Cara terbaik adalah cara Haskell tanpa karakter tambahan sama sekali (lihat inter atas). Saya tidak yakin bagaimana ini berinteraksi dalam tata bahasa PEG.js dengan ; dihilangkan. Mungkin ada kasus ketika f a b = ... dimasukkan (sebagian) ke baris sebelumnya.

digunakan sebagai operator ekspansi dalam beberapa bahasa yang mengimplementasikan arahan preprocess

Ya, tapi saya lebih suka menggunakannya dengan cerdas. Alih-alih mengusulkan tindakan push pada array, saya akan menggunakannya sebagai tindakan Object.assign pada objek, atau bahkan sesuatu yang cocok dengan string kosong ( eps ), tetapi mengembalikan argumennya. Sehingga, misalnya,

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+)

akan mengembalikan {type: "ident", name: "abc"} untuk input "abc" .

Tidak, karena blok kode diharapkan oleh parser untuk mengembalikan objek seperti array yang berisi metode push (sehingga menjadi f = ::"a" { return [ "b" ] } return [ "b", " a" ] sebagai hasilnya),

O_O

Saya pikir ini hanya masalah preferensi saja

Bukan hanya itu, tapi juga performanya. Jika setiap token memiliki _ di kedua sisi, urutan karakter spasi hanya cocok dengan yang tertinggal, sedangkan _ cocok dengan apa pun. Panggilan ekstra ke parse$_ membutuhkan sedikit waktu ekstra. Juga kodenya lebih panjang karena memiliki _ s dua kali lebih banyak.

@Mingun Saya pikir @futagoza mengeksplorasi ruang desain. Itu hal yang baik untuk dilakukan, terutama jika itu publik dan kami memiliki kesempatan untuk tidak setuju :)

Hal utama yang harus dilakukan adalah tidak bertanya "mengapa" tetapi mengatakan "tidak! tidak seperti itu!"

image

f = type:#"ident" name:$([a-z]i [a-z0-9_]i+) akan mengembalikan {type: "ident", name: "abc"} untuk input "abc" .

Tolong jangan, hehe. Sintaks itu _wayyy_ terlalu ajaib.

Saya pikir operator @ awalnya diusulkan sempurna apa adanya. Begitu banyak, berkali-kali saya mengalami masalah urutan:

sequence
    = first:element rest:(whitespace next:element {return next;})*
    {
        return [first].concat(rest);
    }
    ;

_Begitulah_ rasa sakit untuk mengetik berulang-ulang, terutama ketika mereka lebih kompleks dari itu.

Namun, dengan operator @ , di atas menjadi sederhana:

sequence = first:element rest:(whitespace @element)* { return [first].concat(rest); };

dan dengan https://github.com/pegjs/pegjs/issues/235#issuecomment -66915879 atau https://github.com/pegjs/pegjs/issues/235#issuecomment -67544080 yang semakin dikurangi menjadi:

sequence = first:element rest:(whitespace @element)* => [first].concat(rest);
/* or */
sequence = first:element rest:(whitespace @element)* {=[first].concat(rest)};

... yang pertama sangat saya sukai.

Sepertinya ini akan menjadi perubahan yang kompatibel ke belakang yang mudah dicapai (tampaknya seseorang telah melakukannya).

Bahkan, jika saya tidak salah, itu bisa jadi hanya benjolan kecil. Mungkin sesuatu untuk dipikirkan untuk 0.11.0 @futagoza.

Baru saja menambahkan ini ke master. Berencana untuk menggunakan :: untuk pemetikan ganda dan @ untuk pemetikan tunggal, tetapi menggunakan :: dengan label terlihat sangat jelek dan membingungkan, jadi tergantung ide itu

Saya mulai menerapkan ini sendiri beberapa waktu lalu tetapi menjatuhkannya sampai sekarang (ketika saya seharusnya melakukan #579 sebagai gantinya ) dan mendasarkan generator bytecode pada implementasi Mingun (https://github.com/Mingun/pegjs/commit/1c1c852bae91868eaa90d9bd9f7e4f722aa6435e )

Anda dapat mencobanya di sini: https://pegjs.org/development/try (editor online, tetapi menggunakan PEG 0.11.0-dev)

Waktu penyelesaian yang suci, batman. Pekerjaan luar biasa @futagoza , ini bekerja dengan sempurna - sangat dihargai. Apakah ini sudah dirilis ke tag dev di npm? Saya ingin mulai mengujinya.

Bagi siapa saja yang ingin mencobanya dengan tata bahasa dasar, masukkan anak anjing ini ke sana dan berikan masukan seperti "abcd" .

foo
    = '"' @$bar '"'
    ;

bar
    = [abcd]*
    ;

Apakah ini sudah dirilis ke tag dev di npm?

https://www.npmjs.com/package/pegjs/v/0.11.0-dev.325

Hai, ini adalah salah satu masalah yang hilang begitu saja jika kami memiliki es6, dan kami membutuhkan karakter tersebut untuk hal lain yang telah ditambahkan ke es6 sejak itu. Menambahkan operator untuk hal-hal yang sudah dapat Anda lakukan sangat kontraproduktif.

Tiket ini digabungkan

pattern:Pattern init:(_ "=" _ <strong i="7">@a</strong>:AssignmentExpression)?

Hal yang sama di es6, yang secara inheren akan dipahami semua orang, dan yang datang secara gratis ketika bagian lain dari parser selesai, adalah

pattern:Pattern init:(_ "=" _ a:AssignmentExpression)? => a

Masalahnya, ketika diuji, implementasi pluck ini tampaknya buggy, dan tentu saja, ini ditandai ditutup karena ini diperbaiki di cabang yang tidak akan pernah dirilis

Harap buka kembali masalah ini, @futagoza , hingga masalah ini diperbaiki dalam versi yang dirilis

Apakah halaman ini membantu?
0 / 5 - 0 peringkat