pyindo 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
indpy/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
indpy/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
indpy/errors.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ errors.py — Kelas error dengan pesan dalam Bahasa Indonesia.
3
+ Bagian dari proyek Indpy (Python Indonesia).
4
+
5
+ Semua error yang terjadi selama proses transpilasi akan menggunakan
6
+ kelas-kelas di bawah ini agar pesan kesalahan mudah dipahami.
7
+ """
8
+
9
+
10
+ class IndpyError(Exception):
11
+ """Kelas dasar untuk semua error transpiler Indpy."""
12
+
13
+ def __init__(self, pesan: str, baris: int = 0, kolom: int = 0):
14
+ self.pesan = pesan
15
+ self.baris = baris
16
+ self.kolom = kolom
17
+ lokasi = f" (baris {baris}, kolom {kolom})" if baris else ""
18
+ super().__init__(f"[Indpy]{lokasi} {pesan}")
19
+
20
+
21
+ class KesalahanLeksikal(IndpyError):
22
+ """Error pada tahap tokenisasi (lexer)."""
23
+ pass
24
+
25
+
26
+ class KesalahanSintaks(IndpyError):
27
+ """Error pada tahap parsing (parser) — sintaks tidak valid."""
28
+ pass
29
+
30
+
31
+ class KesalahanTranspilasi(IndpyError):
32
+ """Error pada tahap pembangkitan kode (transpiler)."""
33
+ pass
34
+
35
+
36
+ class KesalahanNama(IndpyError):
37
+ """Variabel atau nama yang digunakan belum didefinisikan."""
38
+ pass
39
+
40
+
41
+ # ── Pesan error siap pakai ──────────────────────────────────────────────────
42
+
43
+ def err_karakter_tak_dikenal(karakter: str, baris: int, kolom: int) -> KesalahanLeksikal:
44
+ return KesalahanLeksikal(
45
+ f"Karakter tidak dikenal: '{karakter}'", baris, kolom
46
+ )
47
+
48
+
49
+ def err_string_tidak_ditutup(baris: int, kolom: int) -> KesalahanLeksikal:
50
+ return KesalahanLeksikal(
51
+ "Tanda kutip string tidak ditutup dengan benar.", baris, kolom
52
+ )
53
+
54
+
55
+ def err_token_diharapkan(diharapkan: str, didapat: str, baris: int, kolom: int) -> KesalahanSintaks:
56
+ return KesalahanSintaks(
57
+ f"Diharapkan '{diharapkan}', tetapi mendapat '{didapat}'.", baris, kolom
58
+ )
59
+
60
+
61
+ def err_ekspresi_diharapkan(didapat: str, baris: int, kolom: int) -> KesalahanSintaks:
62
+ return KesalahanSintaks(
63
+ f"Diharapkan ekspresi, tetapi mendapat '{didapat}'.", baris, kolom
64
+ )
65
+
66
+
67
+ def err_indentasi_tak_konsisten(baris: int) -> KesalahanSintaks:
68
+ return KesalahanSintaks(
69
+ "Indentasi tidak konsisten — periksa spasi atau tab pada blok kode.", baris
70
+ )
71
+
72
+
73
+ def err_dedent_tak_cocok(baris: int) -> KesalahanSintaks:
74
+ return KesalahanSintaks(
75
+ "Dedentasi tidak cocok dengan level indentasi sebelumnya.", baris
76
+ )
77
+
78
+
79
+ def err_variabel_belum_ada(nama: str, baris: int) -> KesalahanNama:
80
+ return KesalahanNama(
81
+ f"Nama '{nama}' belum didefinisikan atau tidak dapat ditemukan.", baris
82
+ )
83
+
84
+
85
+ def err_simpul_tak_dikenal(jenis: str) -> KesalahanTranspilasi:
86
+ return KesalahanTranspilasi(
87
+ f"Jenis simpul AST tidak dikenal: '{jenis}'. "
88
+ "Mungkin fitur ini belum didukung."
89
+ )
indpy/keywords.py ADDED
@@ -0,0 +1,96 @@
1
+ """
2
+ keywords.py — Peta kata kunci Bahasa Indonesia ke Python.
3
+ Bagian dari proyek Indpy (Python Indonesia).
4
+
5
+ Modul ini berisi semua kata kunci yang dikenali transpiler,
6
+ termasuk kata kunci kontrol alur, tipe data, dan operator logika.
7
+ """
8
+
9
+ # Peta kata kunci: Bahasa Indonesia -> Python
10
+ KEYWORDS: dict[str, str] = {
11
+ # Definisi & struktur
12
+ "fungsi": "def",
13
+ "kelas": "class",
14
+ "impor": "import",
15
+ "dari": "from",
16
+ "sebagai": "as",
17
+ "kembalikan": "return",
18
+ "kembali": "return", # alias
19
+
20
+ # Kontrol alur
21
+ "jika": "if",
22
+ "lainnya": "else",
23
+ "jika_tidak": "elif",
24
+ "untuk": "for",
25
+ "selama": "while",
26
+ "hentikan": "break",
27
+ "lanjut": "continue",
28
+ "lewati": "pass",
29
+
30
+ # Iterasi
31
+ "dalam": "in",
32
+ "rentang": "range",
33
+
34
+ # Penanganan error
35
+ "coba": "try",
36
+ "kecuali": "except",
37
+ "akhirnya": "finally",
38
+ "naikkan": "raise",
39
+
40
+ # Nilai literal
41
+ "benar": "True",
42
+ "salah": "False",
43
+ "kosong": "None",
44
+
45
+ # Operator logika
46
+ "dan": "and",
47
+ "atau": "or",
48
+ "bukan": "not",
49
+ "tidak": "not", # alias
50
+
51
+ # Fungsi bawaan umum
52
+ "cetak": "print",
53
+ "masukkan": "input",
54
+ "panjang": "len",
55
+ "tipe": "type",
56
+ "bilangan": "int",
57
+ "desimal": "float",
58
+ "teks": "str",
59
+ "daftar": "list",
60
+ "kamus": "dict",
61
+ "himpunan": "set",
62
+ "hapus": "del",
63
+ "bersama": "with",
64
+ "menjadi": "as",
65
+ "global": "global",
66
+ "nonlokal": "nonlocal",
67
+ "lewat": "pass",
68
+ "pernyataan": "assert",
69
+ # yield tidak dipetakan — terlalu ambigu sebagai nama variabel/metode.
70
+ # Gunakan 'yield' Python langsung jika diperlukan.
71
+ "lambda": "lambda",
72
+ }
73
+
74
+ # Sekumpulan kata kunci (untuk pengecekan cepat O(1))
75
+ KEYWORD_SET: set[str] = set(KEYWORDS.keys())
76
+
77
+ # Operator TIGA karakter — harus dicek lebih dulu di lexer (longest-match-first)
78
+ THREE_CHAR_OPS: set[str] = {"**=", "//="}
79
+
80
+ # Operator DUA karakter — dicek setelah THREE_CHAR_OPS
81
+ TWO_CHAR_OPS: set[str] = {
82
+ "==", "!=", "<=", ">=", "+=", "-=", "*=", "/=",
83
+ "//", "**", "->", "<<", ">>", "%=",
84
+ }
85
+
86
+ # Operator satu karakter
87
+ ONE_CHAR_OPS: set[str] = {
88
+ "+", "-", "*", "/", "%", "=", "<", ">", "!",
89
+ "&", "|", "^", "~", "@",
90
+ }
91
+
92
+ # Delimiter (tidak termasuk dalam operator)
93
+ DELIMITERS: set[str] = {
94
+ "(", ")", "[", "]", "{", "}",
95
+ ",", ":", ".", ";",
96
+ }
indpy/lexer.py ADDED
@@ -0,0 +1,400 @@
1
+ """
2
+ lexer.py — Tokenizer (Lexer) untuk Indpy.
3
+ Bagian dari proyek Indpy (Python Indonesia).
4
+
5
+ Mengubah source code teks menjadi aliran token yang terstruktur.
6
+ Tahap ini menangani: keyword, identifier, number, string, operator,
7
+ delimiter, komentar, NEWLINE, INDENT, dan DEDENT.
8
+
9
+ String literal AMAN: teks di dalam tanda kutip tidak akan diterjemahkan.
10
+ """
11
+
12
+ from __future__ import annotations
13
+ from enum import Enum, auto
14
+ from typing import Iterator
15
+
16
+ from .keywords import (
17
+ KEYWORDS, KEYWORD_SET, ONE_CHAR_OPS, TWO_CHAR_OPS, THREE_CHAR_OPS, DELIMITERS
18
+ )
19
+ from .errors import (
20
+ err_karakter_tak_dikenal,
21
+ err_string_tidak_ditutup,
22
+ err_dedent_tak_cocok,
23
+ )
24
+
25
+
26
+ # ── Jenis-jenis Token ────────────────────────────────────────────────────────
27
+
28
+ class TipeToken(Enum):
29
+ # Literal nilai
30
+ ANGKA = auto() # integer atau float: 42, 3.14
31
+ TEKS = auto() # string literal: "halo", 'dunia'
32
+ BENAR = auto() # True / benar
33
+ SALAH = auto() # False / salah
34
+ KOSONG = auto() # None / kosong
35
+
36
+ # Nama & kata kunci
37
+ NAMA = auto() # identifier / nama variabel
38
+ KATA_KUNCI = auto() # kata kunci (sudah dipetakan ke padanan Python)
39
+
40
+ # Operator & tanda baca
41
+ OP = auto() # operator (+, -, ==, <=, dst.)
42
+ DELIMITER = auto() # tanda baca (, : ( ) [ ] { })
43
+
44
+ # Kontrol struktur
45
+ BARIS_BARU = auto() # \n (akhir pernyataan)
46
+ INDENT = auto() # tambah level indentasi
47
+ DEDENT = auto() # kurang level indentasi
48
+
49
+ # Spesial
50
+ EOF = auto() # akhir file
51
+
52
+
53
+ class Token:
54
+ """Satu unit leksikal dari source code."""
55
+
56
+ __slots__ = ("tipe", "nilai", "baris", "kolom")
57
+
58
+ def __init__(self, tipe: TipeToken, nilai: str | int | float,
59
+ baris: int = 0, kolom: int = 0):
60
+ self.tipe = tipe
61
+ self.nilai = nilai
62
+ self.baris = baris
63
+ self.kolom = kolom
64
+
65
+ def __repr__(self) -> str:
66
+ return (
67
+ f"Token({self.tipe.name}, {self.nilai!r}, "
68
+ f"b{self.baris}:k{self.kolom})"
69
+ )
70
+
71
+
72
+ # ── Kelas Lexer ──────────────────────────────────────────────────────────────
73
+
74
+ class Lexer:
75
+ """
76
+ Memproses source code karakter per karakter dan menghasilkan
77
+ daftar token. Mendukung INDENT/DEDENT berbasis spasi.
78
+ """
79
+
80
+ def __init__(self, source: str):
81
+ self.source = source
82
+ self.pos = 0 # posisi karakter saat ini
83
+ self.baris = 1
84
+ self.kolom = 1
85
+ self._indent_stack: list[int] = [0] # tumpukan level indentasi
86
+
87
+ # ── API publik ────────────────────────────────────────────────────────────
88
+
89
+ def tokenisasi(self) -> list[Token]:
90
+ """
91
+ Jalankan seluruh proses tokenisasi dan kembalikan daftar token.
92
+ Menambahkan token EOF di akhir.
93
+ """
94
+ return list(self._iterasi_token())
95
+
96
+ # ── Generator utama ───────────────────────────────────────────────────────
97
+
98
+ def _iterasi_token(self) -> Iterator[Token]:
99
+ """Hasilkan token satu per satu dari source code."""
100
+ awal_baris = True # apakah kita di awal baris baru?
101
+
102
+ while self.pos < len(self.source):
103
+ c = self._karakter()
104
+
105
+ # ── Awal baris: hitung indentasi ──────────────────────────────
106
+ if awal_baris:
107
+ awal_baris = False
108
+ for tok in self._proses_indentasi():
109
+ yield tok
110
+ continue
111
+
112
+ # ── Lewati spasi (bukan awal baris) ──────────────────────────
113
+ if c in (' ', '\t'):
114
+ self._maju()
115
+ continue
116
+
117
+ # ── Komentar (#) ──────────────────────────────────────────────
118
+ if c == '#':
119
+ self._lewati_komentar()
120
+ continue
121
+
122
+ # ── Baris baru ────────────────────────────────────────────────
123
+ if c == '\n':
124
+ b = self.baris
125
+ self._maju()
126
+ self.baris += 1
127
+ self.kolom = 1
128
+ awal_baris = True
129
+ yield Token(TipeToken.BARIS_BARU, "\n", b, self.kolom)
130
+ continue
131
+
132
+ # ── Karakter \ sebagai line continuation ──────────────────────
133
+ if c == '\\' and self._intip(1) == '\n':
134
+ self._maju()
135
+ self._maju()
136
+ self.baris += 1
137
+ self.kolom = 1
138
+ continue
139
+
140
+ # ── String literal (isi string TIDAK diterjemahkan) ───────────
141
+ if c in ('"', "'"):
142
+ yield self._baca_string()
143
+ continue
144
+
145
+ # ── f-string ──────────────────────────────────────────────────
146
+ if c in ('f', 'F', 'r', 'R', 'b', 'B') and self._intip(1) in ('"', "'"):
147
+ yield self._baca_string_prefix()
148
+ continue
149
+
150
+ # ── Angka ─────────────────────────────────────────────────────
151
+ if c.isdigit() or (c == '.' and self._intip(1).isdigit()):
152
+ yield self._baca_angka()
153
+ continue
154
+
155
+ # ── Identifier / kata kunci ───────────────────────────────────
156
+ if c.isalpha() or c == '_':
157
+ yield self._baca_nama()
158
+ continue
159
+
160
+ # ── Operator tiga karakter (longest-match-first) ──────────────
161
+ tiga = self.source[self.pos: self.pos + 3]
162
+ if tiga in THREE_CHAR_OPS:
163
+ tok = Token(TipeToken.OP, tiga, self.baris, self.kolom)
164
+ self._maju(); self._maju(); self._maju()
165
+ yield tok
166
+ continue
167
+
168
+ # ── Operator dua karakter ─────────────────────────────────────
169
+ dua = self.source[self.pos: self.pos + 2]
170
+ if dua in TWO_CHAR_OPS:
171
+ tok = Token(TipeToken.OP, dua, self.baris, self.kolom)
172
+ self._maju()
173
+ self._maju()
174
+ yield tok
175
+ continue
176
+
177
+ # ── Operator satu karakter ────────────────────────────────────
178
+ if c in ONE_CHAR_OPS:
179
+ tok = Token(TipeToken.OP, c, self.baris, self.kolom)
180
+ self._maju()
181
+ yield tok
182
+ continue
183
+
184
+ # ── Delimiter ─────────────────────────────────────────────────
185
+ if c in DELIMITERS:
186
+ tok = Token(TipeToken.DELIMITER, c, self.baris, self.kolom)
187
+ self._maju()
188
+ yield tok
189
+ continue
190
+
191
+ # ── Karakter tidak dikenal ────────────────────────────────────
192
+ raise err_karakter_tak_dikenal(c, self.baris, self.kolom)
193
+
194
+ # ── Akhir file: tutup semua INDENT yang masih terbuka ─────────────
195
+ while len(self._indent_stack) > 1:
196
+ self._indent_stack.pop()
197
+ yield Token(TipeToken.DEDENT, "DEDENT", self.baris, self.kolom)
198
+
199
+ yield Token(TipeToken.EOF, "EOF", self.baris, self.kolom)
200
+
201
+ # ── Pemrosesan indentasi ──────────────────────────────────────────────────
202
+
203
+ def _proses_indentasi(self) -> Iterator[Token]:
204
+ """
205
+ Hitung spasi di awal baris. Hasilkan INDENT atau DEDENT
206
+ sesuai perubahan level. Baris kosong dan baris komentar dilewati.
207
+ """
208
+ spasi = 0
209
+
210
+ while self.pos < len(self.source):
211
+ c = self.source[self.pos]
212
+ if c == ' ':
213
+ spasi += 1
214
+ self._maju()
215
+ elif c == '\t':
216
+ spasi = (spasi // 4 + 1) * 4 # konversi tab ke 4 spasi
217
+ self._maju()
218
+ elif c == '\n':
219
+ # Baris kosong — reset dan mulai ulang
220
+ self._maju()
221
+ self.baris += 1
222
+ self.kolom = 1
223
+ spasi = 0
224
+ elif c == '#':
225
+ # Baris komentar — lewati, lalu mulai ulang
226
+ self._lewati_komentar()
227
+ spasi = 0
228
+ else:
229
+ break
230
+
231
+ prev = self._indent_stack[-1]
232
+
233
+ if spasi > prev:
234
+ self._indent_stack.append(spasi)
235
+ yield Token(TipeToken.INDENT, "INDENT", self.baris, self.kolom)
236
+ elif spasi < prev:
237
+ while self._indent_stack[-1] > spasi:
238
+ self._indent_stack.pop()
239
+ yield Token(TipeToken.DEDENT, "DEDENT", self.baris, self.kolom)
240
+ if self._indent_stack[-1] != spasi:
241
+ raise err_dedent_tak_cocok(self.baris)
242
+
243
+ # ── Pembacaan token individual ────────────────────────────────────────────
244
+
245
+ def _baca_string(self) -> Token:
246
+ """
247
+ Membaca string literal (tunggal, ganda, atau triple-quote).
248
+ Isi string TIDAK diproses untuk penerjemahan kata kunci —
249
+ ini menjamin keamanan: cetak("jika hujan") tetap utuh.
250
+ """
251
+ b, k = self.baris, self.kolom
252
+ pembuka = self._karakter()
253
+ self._maju()
254
+
255
+ # Deteksi triple-quote
256
+ if self.source[self.pos: self.pos + 2] == pembuka * 2:
257
+ self._maju()
258
+ self._maju()
259
+ return self._baca_string_triple(pembuka, b, k)
260
+
261
+ # String biasa (satu baris)
262
+ hasil = pembuka
263
+ while self.pos < len(self.source):
264
+ c = self._karakter()
265
+ if c == '\n':
266
+ raise err_string_tidak_ditutup(b, k)
267
+ if c == '\\':
268
+ hasil += c
269
+ self._maju()
270
+ if self.pos < len(self.source):
271
+ hasil += self._karakter()
272
+ self._maju()
273
+ continue
274
+ hasil += c
275
+ self._maju()
276
+ if c == pembuka:
277
+ break
278
+ else:
279
+ raise err_string_tidak_ditutup(b, k)
280
+
281
+ return Token(TipeToken.TEKS, hasil, b, k)
282
+
283
+ def _baca_string_triple(self, pembuka: str, b: int, k: int) -> Token:
284
+ """Membaca string triple-quote (bisa multi-baris)."""
285
+ penutup = pembuka * 3
286
+ hasil = penutup
287
+
288
+ while self.pos < len(self.source):
289
+ if self.source[self.pos: self.pos + 3] == penutup:
290
+ hasil += penutup
291
+ self._maju()
292
+ self._maju()
293
+ self._maju()
294
+ return Token(TipeToken.TEKS, hasil, b, k)
295
+ c = self._karakter()
296
+ if c == '\n':
297
+ self.baris += 1
298
+ self.kolom = 1
299
+ hasil += c
300
+ self._maju()
301
+
302
+ raise err_string_tidak_ditutup(b, k)
303
+
304
+ def _baca_string_prefix(self) -> Token:
305
+ """Membaca f-string, r-string, atau b-string dengan prefix."""
306
+ b, k = self.baris, self.kolom
307
+ prefix = self._karakter()
308
+ self._maju()
309
+ # Ambil string berikutnya lalu sisipkan prefix
310
+ tok = self._baca_string()
311
+ return Token(TipeToken.TEKS, prefix + str(tok.nilai), b, k)
312
+
313
+ def _baca_angka(self) -> Token:
314
+ """Membaca integer atau float (termasuk format 1_000_000)."""
315
+ b, k = self.baris, self.kolom
316
+ awal = self.pos
317
+ ada_titik = False
318
+
319
+ while self.pos < len(self.source):
320
+ c = self._karakter()
321
+ if c.isdigit() or c == '_':
322
+ self._maju()
323
+ elif c == '.' and not ada_titik and self._intip(1).isdigit():
324
+ ada_titik = True
325
+ self._maju()
326
+ else:
327
+ break
328
+
329
+ teks = self.source[awal: self.pos].replace("_", "")
330
+ nilai: int | float = float(teks) if ada_titik else int(teks)
331
+ return Token(TipeToken.ANGKA, nilai, b, k)
332
+
333
+ def _baca_nama(self) -> Token:
334
+ """
335
+ Membaca identifier. Jika cocok dengan kata kunci Bahasa Indonesia,
336
+ tipe token disesuaikan dan nilai dipetakan ke padanan Python.
337
+ """
338
+ b, k = self.baris, self.kolom
339
+ awal = self.pos
340
+
341
+ while self.pos < len(self.source):
342
+ c = self._karakter()
343
+ if c.isalnum() or c == '_':
344
+ self._maju()
345
+ else:
346
+ break
347
+
348
+ teks = self.source[awal: self.pos]
349
+
350
+ # Kata kunci Bahasa Indonesia → Python
351
+ if teks in KEYWORD_SET:
352
+ padanan = KEYWORDS[teks]
353
+ if padanan == "True":
354
+ return Token(TipeToken.BENAR, teks, b, k)
355
+ if padanan == "False":
356
+ return Token(TipeToken.SALAH, teks, b, k)
357
+ if padanan == "None":
358
+ return Token(TipeToken.KOSONG, teks, b, k)
359
+ return Token(TipeToken.KATA_KUNCI, padanan, b, k)
360
+
361
+ # Kata kunci Python bawaan yang boleh dipakai langsung
362
+ PYTHON_KW = {
363
+ "True", "False", "None", "and", "or", "not",
364
+ "in", "is", "lambda", "yield", "global", "nonlocal",
365
+ "del", "pass", "assert", "raise", "with", "as",
366
+ "import", "from", "def", "class", "return",
367
+ "if", "elif", "else", "while", "for", "break",
368
+ "continue", "try", "except", "finally",
369
+ }
370
+ if teks in PYTHON_KW:
371
+ if teks == "True":
372
+ return Token(TipeToken.BENAR, teks, b, k)
373
+ if teks == "False":
374
+ return Token(TipeToken.SALAH, teks, b, k)
375
+ if teks == "None":
376
+ return Token(TipeToken.KOSONG, teks, b, k)
377
+ return Token(TipeToken.KATA_KUNCI, teks, b, k)
378
+
379
+ return Token(TipeToken.NAMA, teks, b, k)
380
+
381
+ # ── Utilitas ──────────────────────────────────────────────────────────────
382
+
383
+ def _karakter(self) -> str:
384
+ """Karakter pada posisi saat ini (tanpa maju)."""
385
+ return self.source[self.pos] if self.pos < len(self.source) else '\x00'
386
+
387
+ def _intip(self, offset: int = 1) -> str:
388
+ """Intip karakter ke depan tanpa menggerakkan posisi."""
389
+ p = self.pos + offset
390
+ return self.source[p] if p < len(self.source) else '\x00'
391
+
392
+ def _maju(self) -> None:
393
+ """Gerakkan posisi satu karakter ke depan."""
394
+ self.pos += 1
395
+ self.kolom += 1
396
+
397
+ def _lewati_komentar(self) -> None:
398
+ """Lewati semua karakter sampai akhir baris."""
399
+ while self.pos < len(self.source) and self.source[self.pos] != '\n':
400
+ self._maju()