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 +1 -0
- indpy/__main__.py +4 -0
- indpy/errors.py +89 -0
- indpy/keywords.py +96 -0
- indpy/lexer.py +400 -0
- indpy/main.py +371 -0
- indpy/parser.py +845 -0
- indpy/transpiler.py +460 -0
- pyindo-1.0.0.dist-info/METADATA +314 -0
- pyindo-1.0.0.dist-info/RECORD +14 -0
- pyindo-1.0.0.dist-info/WHEEL +5 -0
- pyindo-1.0.0.dist-info/entry_points.txt +2 -0
- pyindo-1.0.0.dist-info/licenses/LICENSE +21 -0
- pyindo-1.0.0.dist-info/top_level.txt +1 -0
indpy/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
indpy/__main__.py
ADDED
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()
|