freelang-v4 4.3.0

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.
Files changed (261) hide show
  1. package/README.md +548 -0
  2. package/dist/ast.d.ts +367 -0
  3. package/dist/ast.js +4 -0
  4. package/dist/ast.js.map +1 -0
  5. package/dist/async-basic.test.d.ts +1 -0
  6. package/dist/async-basic.test.js +88 -0
  7. package/dist/async-basic.test.js.map +1 -0
  8. package/dist/async-jest.test.d.ts +1 -0
  9. package/dist/async-jest.test.js +99 -0
  10. package/dist/async-jest.test.js.map +1 -0
  11. package/dist/channel-jest.test.d.ts +1 -0
  12. package/dist/channel-jest.test.js +148 -0
  13. package/dist/channel-jest.test.js.map +1 -0
  14. package/dist/checker-jest.test.d.ts +1 -0
  15. package/dist/checker-jest.test.js +160 -0
  16. package/dist/checker-jest.test.js.map +1 -0
  17. package/dist/checker.d.ts +149 -0
  18. package/dist/checker.js +1565 -0
  19. package/dist/checker.js.map +1 -0
  20. package/dist/checker.test.d.ts +1 -0
  21. package/dist/checker.test.js +217 -0
  22. package/dist/checker.test.js.map +1 -0
  23. package/dist/compiler-jest.test.d.ts +1 -0
  24. package/dist/compiler-jest.test.js +233 -0
  25. package/dist/compiler-jest.test.js.map +1 -0
  26. package/dist/compiler.d.ts +127 -0
  27. package/dist/compiler.js +1588 -0
  28. package/dist/compiler.js.map +1 -0
  29. package/dist/compiler.test.d.ts +1 -0
  30. package/dist/compiler.test.js +313 -0
  31. package/dist/compiler.test.js.map +1 -0
  32. package/dist/db-100m-full.d.ts +5 -0
  33. package/dist/db-100m-full.js +78 -0
  34. package/dist/db-100m-full.js.map +1 -0
  35. package/dist/db-100m-no-index.d.ts +12 -0
  36. package/dist/db-100m-no-index.js +119 -0
  37. package/dist/db-100m-no-index.js.map +1 -0
  38. package/dist/db-100m-real.d.ts +5 -0
  39. package/dist/db-100m-real.js +131 -0
  40. package/dist/db-100m-real.js.map +1 -0
  41. package/dist/db-100m-streaming.d.ts +15 -0
  42. package/dist/db-100m-streaming.js +164 -0
  43. package/dist/db-100m-streaming.js.map +1 -0
  44. package/dist/db-100m-test.d.ts +5 -0
  45. package/dist/db-100m-test.js +111 -0
  46. package/dist/db-100m-test.js.map +1 -0
  47. package/dist/db-jest.test.d.ts +1 -0
  48. package/dist/db-jest.test.js +182 -0
  49. package/dist/db-jest.test.js.map +1 -0
  50. package/dist/db-runtime.d.ts +24 -0
  51. package/dist/db-runtime.js +204 -0
  52. package/dist/db-runtime.js.map +1 -0
  53. package/dist/db.d.ts +249 -0
  54. package/dist/db.js +593 -0
  55. package/dist/db.js.map +1 -0
  56. package/dist/file-io-jest.test.d.ts +1 -0
  57. package/dist/file-io-jest.test.js +225 -0
  58. package/dist/file-io-jest.test.js.map +1 -0
  59. package/dist/for-of-jest.test.d.ts +1 -0
  60. package/dist/for-of-jest.test.js +230 -0
  61. package/dist/for-of-jest.test.js.map +1 -0
  62. package/dist/for-of.test.d.ts +1 -0
  63. package/dist/for-of.test.js +305 -0
  64. package/dist/for-of.test.js.map +1 -0
  65. package/dist/function-literal-jest.test.d.ts +1 -0
  66. package/dist/function-literal-jest.test.js +180 -0
  67. package/dist/function-literal-jest.test.js.map +1 -0
  68. package/dist/function-literal.test.d.ts +1 -0
  69. package/dist/function-literal.test.js +245 -0
  70. package/dist/function-literal.test.js.map +1 -0
  71. package/dist/generics-jest.test.d.ts +1 -0
  72. package/dist/generics-jest.test.js +93 -0
  73. package/dist/generics-jest.test.js.map +1 -0
  74. package/dist/ir-gen.d.ts +15 -0
  75. package/dist/ir-gen.js +400 -0
  76. package/dist/ir-gen.js.map +1 -0
  77. package/dist/ir.d.ts +114 -0
  78. package/dist/ir.js +5 -0
  79. package/dist/ir.js.map +1 -0
  80. package/dist/lexer.d.ts +110 -0
  81. package/dist/lexer.js +467 -0
  82. package/dist/lexer.js.map +1 -0
  83. package/dist/lexer.test.d.ts +1 -0
  84. package/dist/lexer.test.js +426 -0
  85. package/dist/lexer.test.js.map +1 -0
  86. package/dist/main.d.ts +2 -0
  87. package/dist/main.js +241 -0
  88. package/dist/main.js.map +1 -0
  89. package/dist/module-jest.test.d.ts +1 -0
  90. package/dist/module-jest.test.js +123 -0
  91. package/dist/module-jest.test.js.map +1 -0
  92. package/dist/parser.d.ts +56 -0
  93. package/dist/parser.js +1060 -0
  94. package/dist/parser.js.map +1 -0
  95. package/dist/parser.test.d.ts +1 -0
  96. package/dist/parser.test.js +461 -0
  97. package/dist/parser.test.js.map +1 -0
  98. package/dist/pattern-matching-jest.test.d.ts +1 -0
  99. package/dist/pattern-matching-jest.test.js +158 -0
  100. package/dist/pattern-matching-jest.test.js.map +1 -0
  101. package/dist/pkg/init.d.ts +1 -0
  102. package/dist/pkg/init.js +118 -0
  103. package/dist/pkg/init.js.map +1 -0
  104. package/dist/pkg/install.d.ts +1 -0
  105. package/dist/pkg/install.js +77 -0
  106. package/dist/pkg/install.js.map +1 -0
  107. package/dist/pkg/registry.d.ts +23 -0
  108. package/dist/pkg/registry.js +106 -0
  109. package/dist/pkg/registry.js.map +1 -0
  110. package/dist/pkg/run.d.ts +1 -0
  111. package/dist/pkg/run.js +76 -0
  112. package/dist/pkg/run.js.map +1 -0
  113. package/dist/pkg/toml.d.ts +5 -0
  114. package/dist/pkg/toml.js +117 -0
  115. package/dist/pkg/toml.js.map +1 -0
  116. package/dist/repl.d.ts +15 -0
  117. package/dist/repl.js +197 -0
  118. package/dist/repl.js.map +1 -0
  119. package/dist/runtime/bytecode.d.ts +92 -0
  120. package/dist/runtime/bytecode.js +253 -0
  121. package/dist/runtime/bytecode.js.map +1 -0
  122. package/dist/runtime/value.d.ts +102 -0
  123. package/dist/runtime/value.js +302 -0
  124. package/dist/runtime/value.js.map +1 -0
  125. package/dist/runtime/vm.d.ts +65 -0
  126. package/dist/runtime/vm.js +293 -0
  127. package/dist/runtime/vm.js.map +1 -0
  128. package/dist/struct-instance-jest.test.d.ts +1 -0
  129. package/dist/struct-instance-jest.test.js +209 -0
  130. package/dist/struct-instance-jest.test.js.map +1 -0
  131. package/dist/struct-instance.test.d.ts +1 -0
  132. package/dist/struct-instance.test.js +291 -0
  133. package/dist/struct-instance.test.js.map +1 -0
  134. package/dist/struct-jest.test.d.ts +1 -0
  135. package/dist/struct-jest.test.js +176 -0
  136. package/dist/struct-jest.test.js.map +1 -0
  137. package/dist/struct.test.d.ts +1 -0
  138. package/dist/struct.test.js +231 -0
  139. package/dist/struct.test.js.map +1 -0
  140. package/dist/trait-jest.test.d.ts +1 -0
  141. package/dist/trait-jest.test.js +120 -0
  142. package/dist/trait-jest.test.js.map +1 -0
  143. package/dist/vm-jest.test.d.ts +1 -0
  144. package/dist/vm-jest.test.js +569 -0
  145. package/dist/vm-jest.test.js.map +1 -0
  146. package/dist/vm.d.ts +81 -0
  147. package/dist/vm.js +1956 -0
  148. package/dist/vm.js.map +1 -0
  149. package/dist/vm.test.d.ts +1 -0
  150. package/dist/vm.test.js +337 -0
  151. package/dist/vm.test.js.map +1 -0
  152. package/dist/web-repl/sandbox.d.ts +11 -0
  153. package/dist/web-repl/sandbox.js +76 -0
  154. package/dist/web-repl/sandbox.js.map +1 -0
  155. package/dist/web-repl/server.d.ts +1 -0
  156. package/dist/web-repl/server.js +111 -0
  157. package/dist/web-repl/server.js.map +1 -0
  158. package/dist/while-loop-jest.test.d.ts +1 -0
  159. package/dist/while-loop-jest.test.js +201 -0
  160. package/dist/while-loop-jest.test.js.map +1 -0
  161. package/dist/while-loop.test.d.ts +1 -0
  162. package/dist/while-loop.test.js +262 -0
  163. package/dist/while-loop.test.js.map +1 -0
  164. package/docs/EXPERIENCE.md +787 -0
  165. package/docs/README.md +175 -0
  166. package/docs/V1_V2_V3_ANALYSIS.md +107 -0
  167. package/docs/_config.yml +36 -0
  168. package/docs/api-reference.md +459 -0
  169. package/docs/architecture.md +470 -0
  170. package/docs/benchmarks.md +295 -0
  171. package/docs/comparison.md +454 -0
  172. package/docs/index.md +335 -0
  173. package/docs/language-completeness.md +228 -0
  174. package/docs/learning-guide.md +651 -0
  175. package/package.json +65 -0
  176. package/src/api/deploy_key.fl +294 -0
  177. package/src/api/issue.fl +302 -0
  178. package/src/api/org.fl +356 -0
  179. package/src/api/repo.fl +394 -0
  180. package/src/api/team.fl +299 -0
  181. package/src/api/user.fl +385 -0
  182. package/src/api/webhook.fl +273 -0
  183. package/src/ast.ts +158 -0
  184. package/src/async-basic.test.ts +94 -0
  185. package/src/async-jest.test.ts +107 -0
  186. package/src/channel-jest.test.ts +158 -0
  187. package/src/checker-jest.test.ts +189 -0
  188. package/src/checker.test.ts +279 -0
  189. package/src/checker.ts +1861 -0
  190. package/src/commands/analyze.fl +227 -0
  191. package/src/commands/auth.fl +315 -0
  192. package/src/commands/batch.fl +349 -0
  193. package/src/commands/config.fl +199 -0
  194. package/src/commands/deploy_key.fl +352 -0
  195. package/src/commands/issue.fl +275 -0
  196. package/src/commands/main.fl +492 -0
  197. package/src/commands/org.fl +425 -0
  198. package/src/commands/repo.fl +581 -0
  199. package/src/commands/team.fl +244 -0
  200. package/src/commands/user.fl +423 -0
  201. package/src/commands/webhook.fl +400 -0
  202. package/src/compiler-jest.test.ts +275 -0
  203. package/src/compiler.test.ts +375 -0
  204. package/src/compiler.ts +1770 -0
  205. package/src/config.fl +175 -0
  206. package/src/core/batch.fl +355 -0
  207. package/src/core/cache.fl +284 -0
  208. package/src/core/ensure.fl +324 -0
  209. package/src/db-100m-full.ts +96 -0
  210. package/src/db-100m-no-index.ts +133 -0
  211. package/src/db-100m-real.ts +152 -0
  212. package/src/db-100m-streaming.ts +154 -0
  213. package/src/db-100m-test.ts +136 -0
  214. package/src/db-jest.test.ts +161 -0
  215. package/src/db-runtime.ts +242 -0
  216. package/src/db.ts +676 -0
  217. package/src/errors.fl +134 -0
  218. package/src/for-of-jest.test.ts +246 -0
  219. package/src/for-of.test.ts +308 -0
  220. package/src/function-literal-jest.test.ts +193 -0
  221. package/src/function-literal.test.ts +248 -0
  222. package/src/generics-jest.test.ts +104 -0
  223. package/src/http/client.fl +327 -0
  224. package/src/ir-gen.ts +459 -0
  225. package/src/ir.ts +80 -0
  226. package/src/lexer.test.ts +499 -0
  227. package/src/lexer.ts +522 -0
  228. package/src/main.ts +223 -0
  229. package/src/models.fl +162 -0
  230. package/src/module-jest.test.ts +145 -0
  231. package/src/parser.test.ts +542 -0
  232. package/src/parser.ts +1211 -0
  233. package/src/pattern-matching-jest.test.ts +170 -0
  234. package/src/pkg/init.ts +91 -0
  235. package/src/pkg/install.ts +56 -0
  236. package/src/pkg/registry.ts +103 -0
  237. package/src/pkg/run.ts +49 -0
  238. package/src/pkg/toml.ts +129 -0
  239. package/src/repl.ts +190 -0
  240. package/src/runtime/bytecode.ts +291 -0
  241. package/src/runtime/value.ts +322 -0
  242. package/src/runtime/vm.ts +354 -0
  243. package/src/self-host/bootstrap.fl +68 -0
  244. package/src/self-host/interpreter.fl +361 -0
  245. package/src/self-host/lexer-simple.fl +22 -0
  246. package/src/self-host/lexer.fl +305 -0
  247. package/src/self-host/parser.fl +580 -0
  248. package/src/struct-instance-jest.test.ts +221 -0
  249. package/src/struct-instance.test.ts +293 -0
  250. package/src/struct-jest.test.ts +187 -0
  251. package/src/struct.test.ts +234 -0
  252. package/src/trait-jest.test.ts +136 -0
  253. package/src/vm-jest.test.ts +754 -0
  254. package/src/vm.ts +1976 -0
  255. package/src/web-repl/public/index.html +50 -0
  256. package/src/web-repl/public/main.js +105 -0
  257. package/src/web-repl/public/style.css +225 -0
  258. package/src/web-repl/sandbox.ts +88 -0
  259. package/src/web-repl/server.ts +97 -0
  260. package/src/while-loop-jest.test.ts +218 -0
  261. package/src/while-loop.test.ts +267 -0
package/src/lexer.ts ADDED
@@ -0,0 +1,522 @@
1
+ // FreeLang v4 — Lexer (SPEC_04 구현)
2
+ // UTF-8 소스코드 → 토큰 배열
3
+
4
+ // ============================================================
5
+ // TokenType — 6 카테고리, ~50종
6
+ // ============================================================
7
+
8
+ export enum TokenType {
9
+ // 키워드 (23)
10
+ VAR = "VAR",
11
+ LET = "LET",
12
+ CONST = "CONST",
13
+ FN = "FN",
14
+ ASYNC = "ASYNC",
15
+ AWAIT = "AWAIT",
16
+ STRUCT = "STRUCT",
17
+ TRAIT = "TRAIT",
18
+ IMPL = "IMPL",
19
+ IF = "IF",
20
+ ELSE = "ELSE",
21
+ MATCH = "MATCH",
22
+ FOR = "FOR",
23
+ IN = "IN",
24
+ OF = "OF",
25
+ WHILE = "WHILE",
26
+ BREAK = "BREAK",
27
+ CONTINUE = "CONTINUE",
28
+ RETURN = "RETURN",
29
+ SPAWN = "SPAWN",
30
+ TRUE = "TRUE",
31
+ FALSE = "FALSE",
32
+ AS = "AS",
33
+ IMPORT = "IMPORT",
34
+ EXPORT = "EXPORT",
35
+ FROM = "FROM",
36
+
37
+ // 타입 이름 (7)
38
+ TYPE_I32 = "TYPE_I32",
39
+ TYPE_I64 = "TYPE_I64",
40
+ TYPE_F64 = "TYPE_F64",
41
+ TYPE_BOOL = "TYPE_BOOL",
42
+ TYPE_STRING = "TYPE_STRING",
43
+ TYPE_VOID = "TYPE_VOID",
44
+ TYPE_CHANNEL = "TYPE_CHANNEL",
45
+
46
+ // 리터럴 (3)
47
+ INT_LIT = "INT_LIT",
48
+ FLOAT_LIT = "FLOAT_LIT",
49
+ STRING_LIT = "STRING_LIT",
50
+
51
+ // 연산자/구두점 (27)
52
+ PLUS = "PLUS", // +
53
+ MINUS = "MINUS", // -
54
+ STAR = "STAR", // *
55
+ SLASH = "SLASH", // /
56
+ PERCENT = "PERCENT", // %
57
+ EQ = "EQ", // =
58
+ EQEQ = "EQEQ", // ==
59
+ NEQ = "NEQ", // !=
60
+ LT = "LT", // <
61
+ GT = "GT", // >
62
+ LTEQ = "LTEQ", // <=
63
+ GTEQ = "GTEQ", // >=
64
+ AND = "AND", // &&
65
+ OR = "OR", // ||
66
+ NOT = "NOT", // !
67
+ QUESTION = "QUESTION", // ?
68
+ ARROW = "ARROW", // =>
69
+ RARROW = "RARROW", // ->
70
+ LARROW = "LARROW", // <-
71
+ COLON = "COLON", // :
72
+ COMMA = "COMMA", // ,
73
+ DOT = "DOT", // .
74
+ DOTDOT = "DOTDOT", // ..
75
+ LPAREN = "LPAREN", // (
76
+ RPAREN = "RPAREN", // )
77
+ LBRACKET = "LBRACKET", // [
78
+ RBRACKET = "RBRACKET", // ]
79
+ LBRACE = "LBRACE", // {
80
+ RBRACE = "RBRACE", // }
81
+ SEMICOLON = "SEMICOLON", // ;
82
+
83
+ // 특수 (2) — NEWLINE 토큰 없음 (SPEC_04 Q7)
84
+ IDENT = "IDENT",
85
+ EOF = "EOF",
86
+ }
87
+
88
+ // ============================================================
89
+ // Token
90
+ // ============================================================
91
+
92
+ export type Token = {
93
+ type: TokenType;
94
+ lexeme: string;
95
+ line: number;
96
+ col: number;
97
+ };
98
+
99
+ // ============================================================
100
+ // LexError
101
+ // ============================================================
102
+
103
+ export type LexError = {
104
+ message: string;
105
+ line: number;
106
+ col: number;
107
+ };
108
+
109
+ // ============================================================
110
+ // 키워드 테이블 (SPEC_04 Q4)
111
+ // ============================================================
112
+
113
+ const KEYWORDS: Map<string, TokenType> = new Map([
114
+ // 키워드 (23)
115
+ ["var", TokenType.VAR],
116
+ ["let", TokenType.LET],
117
+ ["const", TokenType.CONST],
118
+ ["fn", TokenType.FN],
119
+ ["async", TokenType.ASYNC],
120
+ ["await", TokenType.AWAIT],
121
+ ["struct", TokenType.STRUCT],
122
+ ["trait", TokenType.TRAIT],
123
+ ["impl", TokenType.IMPL],
124
+ ["if", TokenType.IF],
125
+ ["else", TokenType.ELSE],
126
+ ["match", TokenType.MATCH],
127
+ ["for", TokenType.FOR],
128
+ ["in", TokenType.IN],
129
+ ["of", TokenType.OF],
130
+ ["while", TokenType.WHILE],
131
+ ["break", TokenType.BREAK],
132
+ ["continue", TokenType.CONTINUE],
133
+ ["return", TokenType.RETURN],
134
+ ["spawn", TokenType.SPAWN],
135
+ ["true", TokenType.TRUE],
136
+ ["false", TokenType.FALSE],
137
+ ["as", TokenType.AS],
138
+ // 모듈 시스템 키워드 (3)
139
+ ["import", TokenType.IMPORT],
140
+ ["export", TokenType.EXPORT],
141
+ ["from", TokenType.FROM],
142
+ // 논리 연산자 키워드 (2) — && || 대신 사용 가능
143
+ ["and", TokenType.AND],
144
+ ["or", TokenType.OR],
145
+ // 타입 이름 (7)
146
+ ["i32", TokenType.TYPE_I32],
147
+ ["i64", TokenType.TYPE_I64],
148
+ ["f64", TokenType.TYPE_F64],
149
+ ["bool", TokenType.TYPE_BOOL],
150
+ ["string", TokenType.TYPE_STRING],
151
+ ["void", TokenType.TYPE_VOID],
152
+ ["channel", TokenType.TYPE_CHANNEL],
153
+ ]);
154
+
155
+ // Option, Result, Ok, Err, Some, None은 키워드가 아님 → IDENT
156
+
157
+ // ============================================================
158
+ // Lexer
159
+ // ============================================================
160
+
161
+ export class Lexer {
162
+ private source: string;
163
+ private pos: number = 0;
164
+ private line: number = 1;
165
+ private col: number = 1;
166
+ private tokens: Token[] = [];
167
+ private errors: LexError[] = [];
168
+
169
+ constructor(source: string) {
170
+ this.source = source;
171
+ }
172
+
173
+ tokenize(): { tokens: Token[]; errors: LexError[] } {
174
+ while (!this.isAtEnd()) {
175
+ this.scanToken();
176
+ }
177
+ this.tokens.push({ type: TokenType.EOF, lexeme: "", line: this.line, col: this.col });
178
+ return { tokens: this.tokens, errors: this.errors };
179
+ }
180
+
181
+ private scanToken(): void {
182
+ const ch = this.peek();
183
+
184
+ // 공백 건너뜀 (스펙: ' ', '\t', '\r')
185
+ if (ch === " " || ch === "\t" || ch === "\r") {
186
+ this.advance();
187
+ return;
188
+ }
189
+
190
+ // 줄바꿈 — 토큰 생성 안 함, 줄 번호만 추적 (SPEC_04 Q7)
191
+ if (ch === "\n") {
192
+ this.advance();
193
+ this.line++;
194
+ this.col = 1;
195
+ return;
196
+ }
197
+
198
+ // # 주석 (FreeLang 고유)
199
+ if (ch === "#") {
200
+ this.skipHashComment();
201
+ return;
202
+ }
203
+
204
+ // 주석 또는 SLASH (SPEC_04 Q8)
205
+ if (ch === "/") {
206
+ if (this.peekNext() === "/") {
207
+ this.skipLineComment();
208
+ return;
209
+ }
210
+ if (this.peekNext() === "*") {
211
+ this.addError("block comments not supported, use //");
212
+ this.advance(); // /
213
+ this.advance(); // *
214
+ return;
215
+ }
216
+ this.addToken(TokenType.SLASH, "/");
217
+ this.advance();
218
+ return;
219
+ }
220
+
221
+ // 식별자 또는 키워드 (SPEC_04 Q4)
222
+ if (this.isAlpha(ch)) {
223
+ this.scanIdentOrKeyword();
224
+ return;
225
+ }
226
+
227
+ // 숫자 리터럴 (SPEC_04 Q5)
228
+ if (this.isDigit(ch)) {
229
+ this.scanNumber();
230
+ return;
231
+ }
232
+
233
+ // 문자열 리터럴 (SPEC_04 Q6)
234
+ if (ch === '"') {
235
+ this.scanString();
236
+ return;
237
+ }
238
+
239
+ // 2글자 연산자 먼저 확인 (최장 일치, SPEC_04 Q3)
240
+ const startLine = this.line;
241
+ const startCol = this.col;
242
+
243
+ if (ch === "=" && this.peekNext() === "=") {
244
+ this.advance(); this.advance();
245
+ this.tokens.push({ type: TokenType.EQEQ, lexeme: "==", line: startLine, col: startCol });
246
+ return;
247
+ }
248
+ if (ch === "=" && this.peekNext() === ">") {
249
+ this.advance(); this.advance();
250
+ this.tokens.push({ type: TokenType.ARROW, lexeme: "=>", line: startLine, col: startCol });
251
+ return;
252
+ }
253
+ if (ch === "-" && this.peekNext() === ">") {
254
+ this.advance(); this.advance();
255
+ this.tokens.push({ type: TokenType.RARROW, lexeme: "->", line: startLine, col: startCol });
256
+ return;
257
+ }
258
+ if (ch === "!" && this.peekNext() === "=") {
259
+ this.advance(); this.advance();
260
+ this.tokens.push({ type: TokenType.NEQ, lexeme: "!=", line: startLine, col: startCol });
261
+ return;
262
+ }
263
+ if (ch === "<" && this.peekNext() === "=") {
264
+ this.advance(); this.advance();
265
+ this.tokens.push({ type: TokenType.LTEQ, lexeme: "<=", line: startLine, col: startCol });
266
+ return;
267
+ }
268
+ if (ch === "<" && this.peekNext() === "-") {
269
+ this.advance(); this.advance();
270
+ this.tokens.push({ type: TokenType.LARROW, lexeme: "<-", line: startLine, col: startCol });
271
+ return;
272
+ }
273
+ if (ch === ">" && this.peekNext() === "=") {
274
+ this.advance(); this.advance();
275
+ this.tokens.push({ type: TokenType.GTEQ, lexeme: ">=", line: startLine, col: startCol });
276
+ return;
277
+ }
278
+ if (ch === "&" && this.peekNext() === "&") {
279
+ this.advance(); this.advance();
280
+ this.tokens.push({ type: TokenType.AND, lexeme: "&&", line: startLine, col: startCol });
281
+ return;
282
+ }
283
+ if (ch === "|" && this.peekNext() === "|") {
284
+ this.advance(); this.advance();
285
+ this.tokens.push({ type: TokenType.OR, lexeme: "||", line: startLine, col: startCol });
286
+ return;
287
+ }
288
+ if (ch === "." && this.peekNext() === ".") {
289
+ this.advance(); this.advance();
290
+ this.tokens.push({ type: TokenType.DOTDOT, lexeme: "..", line: startLine, col: startCol });
291
+ return;
292
+ }
293
+
294
+ // 1글자 연산자/구두점
295
+ const singleCharTokens: Record<string, TokenType> = {
296
+ "+": TokenType.PLUS,
297
+ "-": TokenType.MINUS,
298
+ "*": TokenType.STAR,
299
+ "%": TokenType.PERCENT,
300
+ "=": TokenType.EQ,
301
+ "<": TokenType.LT,
302
+ ">": TokenType.GT,
303
+ "!": TokenType.NOT,
304
+ "?": TokenType.QUESTION,
305
+ ":": TokenType.COLON,
306
+ ",": TokenType.COMMA,
307
+ ".": TokenType.DOT,
308
+ ";": TokenType.SEMICOLON,
309
+ "(": TokenType.LPAREN,
310
+ ")": TokenType.RPAREN,
311
+ "[": TokenType.LBRACKET,
312
+ "]": TokenType.RBRACKET,
313
+ "{": TokenType.LBRACE,
314
+ "}": TokenType.RBRACE,
315
+ };
316
+
317
+ const tokenType = singleCharTokens[ch];
318
+ if (tokenType !== undefined) {
319
+ this.addToken(tokenType, ch);
320
+ this.advance();
321
+ return;
322
+ }
323
+
324
+ // 알 수 없는 문자
325
+ this.addError(`unexpected character: ${ch}`);
326
+ this.advance();
327
+ }
328
+
329
+ // --------------------------------------------------------
330
+ // 식별자/키워드 스캔 (SPEC_04 Q4)
331
+ // --------------------------------------------------------
332
+
333
+ private scanIdentOrKeyword(): void {
334
+ const startCol = this.col;
335
+ const start = this.pos;
336
+
337
+ while (!this.isAtEnd() && this.isAlphaNum(this.peek())) {
338
+ this.advance();
339
+ }
340
+
341
+ const text = this.source.slice(start, this.pos);
342
+ const type = KEYWORDS.get(text) ?? TokenType.IDENT;
343
+ this.tokens.push({ type, lexeme: text, line: this.line, col: startCol });
344
+ }
345
+
346
+ // --------------------------------------------------------
347
+ // 숫자 스캔 (SPEC_04 Q5)
348
+ // --------------------------------------------------------
349
+
350
+ private scanNumber(): void {
351
+ const startCol = this.col;
352
+ const start = this.pos;
353
+ let isFloat = false;
354
+
355
+ // 정수부: [0-9][0-9_]*
356
+ this.consumeDigits();
357
+
358
+ // 소수점 확인: '.' 다음이 숫자면 FLOAT_LIT
359
+ if (!this.isAtEnd() && this.peek() === "." && this.isDigit(this.peekNext() ?? "")) {
360
+ isFloat = true;
361
+ this.advance(); // '.' 소비
362
+ this.consumeDigits();
363
+ }
364
+
365
+ const text = this.source.slice(start, this.pos);
366
+
367
+ // 밑줄 유효성 검사
368
+ if (text.endsWith("_")) {
369
+ this.errors.push({ message: "trailing underscore in number", line: this.line, col: startCol });
370
+ return;
371
+ }
372
+ if (text.includes("__")) {
373
+ this.errors.push({ message: "consecutive underscores in number", line: this.line, col: startCol });
374
+ return;
375
+ }
376
+
377
+ const type = isFloat ? TokenType.FLOAT_LIT : TokenType.INT_LIT;
378
+ this.tokens.push({ type, lexeme: text, line: this.line, col: startCol });
379
+ }
380
+
381
+ private consumeDigits(): void {
382
+ while (!this.isAtEnd() && (this.isDigit(this.peek()) || this.peek() === "_")) {
383
+ this.advance();
384
+ }
385
+ }
386
+
387
+ // --------------------------------------------------------
388
+ // 문자열 스캔 (SPEC_04 Q6)
389
+ // --------------------------------------------------------
390
+
391
+ private scanString(): void {
392
+ const startLine = this.line;
393
+ const startCol = this.col;
394
+ this.advance(); // 여는 '"' 소비
395
+
396
+ let value = "";
397
+
398
+ while (!this.isAtEnd() && this.peek() !== '"') {
399
+ const ch = this.peek();
400
+
401
+ // 줄바꿈 불허
402
+ if (ch === "\n") {
403
+ this.errors.push({
404
+ message: "newline in string literal, use \\n",
405
+ line: this.line,
406
+ col: this.col,
407
+ });
408
+ return;
409
+ }
410
+
411
+ // 이스케이프 시퀀스
412
+ if (ch === "\\") {
413
+ this.advance(); // '\' 소비
414
+ if (this.isAtEnd()) {
415
+ this.errors.push({ message: "unterminated string literal", line: startLine, col: startCol });
416
+ return;
417
+ }
418
+ const esc = this.peek();
419
+ switch (esc) {
420
+ case "n": value += "\n"; break;
421
+ case "t": value += "\t"; break;
422
+ case "r": value += "\r"; break;
423
+ case "\\": value += "\\"; break;
424
+ case '"': value += '"'; break;
425
+ case "0": value += "\0"; break;
426
+ default:
427
+ this.errors.push({
428
+ message: `unknown escape sequence: \\${esc}`,
429
+ line: this.line,
430
+ col: this.col - 1,
431
+ });
432
+ value += esc;
433
+ }
434
+ this.advance();
435
+ continue;
436
+ }
437
+
438
+ value += ch;
439
+ this.advance();
440
+ }
441
+
442
+ if (this.isAtEnd()) {
443
+ this.errors.push({ message: "unterminated string literal", line: startLine, col: startCol });
444
+ return;
445
+ }
446
+
447
+ this.advance(); // 닫는 '"' 소비
448
+
449
+ const lexeme = this.source.slice(this.source.indexOf('"', this.pos - value.length - 2 < 0 ? 0 : 0), this.pos);
450
+ this.tokens.push({ type: TokenType.STRING_LIT, lexeme: value, line: startLine, col: startCol });
451
+ }
452
+
453
+ // --------------------------------------------------------
454
+ // 주석 스캔 (SPEC_04 Q8)
455
+ // --------------------------------------------------------
456
+
457
+ private skipLineComment(): void {
458
+ this.advance(); // 첫 번째 '/'
459
+ this.advance(); // 두 번째 '/'
460
+ while (!this.isAtEnd() && this.peek() !== "\n") {
461
+ this.advance();
462
+ }
463
+ // '\n'은 소비하지 않음 — scanToken에서 줄 번호 처리
464
+ }
465
+
466
+ // # 주석 스캔 (FreeLang)
467
+ private skipHashComment(): void {
468
+ this.advance(); // '#' 소비
469
+ while (!this.isAtEnd() && this.peek() !== "\n") {
470
+ this.advance();
471
+ }
472
+ // '\n'은 소비하지 않음 — scanToken에서 줄 번호 처리
473
+ }
474
+
475
+ // --------------------------------------------------------
476
+ // 유틸리티
477
+ // --------------------------------------------------------
478
+
479
+ private isAtEnd(): boolean {
480
+ return this.pos >= this.source.length;
481
+ }
482
+
483
+ private peek(): string {
484
+ return this.source[this.pos];
485
+ }
486
+
487
+ private peekNext(): string | undefined {
488
+ return this.pos + 1 < this.source.length ? this.source[this.pos + 1] : undefined;
489
+ }
490
+
491
+ private advance(): void {
492
+ if (!this.isAtEnd()) {
493
+ this.pos++;
494
+ this.col++;
495
+ }
496
+ }
497
+
498
+ private isAlpha(ch: string): boolean {
499
+ if ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || ch === "_") {
500
+ return true;
501
+ }
502
+ // 한글 지원: U+AC00 ~ U+D7A3 (가 ~ 힣)
503
+ const code = ch.charCodeAt(0);
504
+ return code >= 0xAC00 && code <= 0xD7A3;
505
+ }
506
+
507
+ private isDigit(ch: string): boolean {
508
+ return ch >= "0" && ch <= "9";
509
+ }
510
+
511
+ private isAlphaNum(ch: string): boolean {
512
+ return this.isAlpha(ch) || this.isDigit(ch);
513
+ }
514
+
515
+ private addToken(type: TokenType, lexeme: string): void {
516
+ this.tokens.push({ type, lexeme, line: this.line, col: this.col });
517
+ }
518
+
519
+ private addError(message: string): void {
520
+ this.errors.push({ message, line: this.line, col: this.col });
521
+ }
522
+ }