@tishlang/tish-format 1.0.12 → 1.0.13

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 (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,716 @@
1
+ //! Tish lexer with indent normalization and tab/space handling.
2
+ //!
3
+ //! Normalizes tabs and spaces to a single indent level so both styles work.
4
+ //! Emits virtual Indent/Dedent tokens for optional-brace blocks.
5
+
6
+ mod token;
7
+
8
+ pub use token::{Span, Token, TokenKind};
9
+
10
+ use std::collections::VecDeque;
11
+ use std::iter::Peekable;
12
+ use std::str::Chars;
13
+
14
+ const INDENT_WIDTH: usize = 2;
15
+ const TAB_AS_LEVELS: usize = 1;
16
+
17
+ /// One JSX element on the stack: tracks whether we are still in its opening tag (`<Tag ...`)
18
+ /// and how many `{` are open inside that element's **attribute values** (embedded JS).
19
+ /// This lets `>` be a comparison operator inside `{...}` while still closing `<span>` when
20
+ /// `attr_value_braces == 0` for the innermost element (React-like).
21
+ #[derive(Debug, Clone)]
22
+ struct JsxEl {
23
+ in_opener: bool,
24
+ attr_value_braces: i32,
25
+ }
26
+
27
+ #[derive(Debug, Clone)]
28
+ pub struct Lexer<'a> {
29
+ chars: Peekable<Chars<'a>>,
30
+ pos: usize,
31
+ line: usize,
32
+ col: usize,
33
+ indent_stack: Vec<usize>,
34
+ at_line_start: bool,
35
+ pending_dedents: VecDeque<Token>,
36
+ template_brace_stack: Vec<usize>,
37
+ jsx_after_gt: bool,
38
+ jsx_in_opening_tag: bool,
39
+ jsx_saw_slash_before_gt: bool,
40
+ jsx_stack: Vec<JsxEl>,
41
+ jsx_depth: i32,
42
+ jsx_child_brace_depth: i32,
43
+ jsx_in_closing_tag: bool,
44
+ }
45
+
46
+ impl<'a> Lexer<'a> {
47
+ pub fn new(source: &'a str) -> Self {
48
+ Self {
49
+ chars: source.chars().peekable(),
50
+ pos: 0,
51
+ line: 1,
52
+ col: 1,
53
+ indent_stack: vec![0],
54
+ at_line_start: true,
55
+ pending_dedents: VecDeque::new(),
56
+ template_brace_stack: Vec::new(),
57
+ jsx_after_gt: false,
58
+ jsx_in_opening_tag: false,
59
+ jsx_saw_slash_before_gt: false,
60
+ jsx_stack: Vec::new(),
61
+ jsx_depth: 0,
62
+ jsx_child_brace_depth: 0,
63
+ jsx_in_closing_tag: false,
64
+ }
65
+ }
66
+
67
+ #[inline]
68
+ fn jsx_sync_in_opening_tag(&mut self) {
69
+ self.jsx_in_opening_tag = self.jsx_stack.last().map(|e| e.in_opener).unwrap_or(false);
70
+ }
71
+
72
+ fn read_jsx_text(&mut self, start: (usize, usize)) -> Result<Option<Token>, String> {
73
+ let mut s = String::new();
74
+ loop {
75
+ match self.peek() {
76
+ None | Some('{') | Some('<') => break,
77
+ Some(c) => {
78
+ self.advance();
79
+ s.push(c);
80
+ }
81
+ }
82
+ }
83
+ if s.is_empty() {
84
+ Ok(None)
85
+ } else {
86
+ let end = self.span_start();
87
+ Ok(Some(Token {
88
+ kind: TokenKind::JsxText,
89
+ span: Span { start, end },
90
+ literal: Some(s.into()),
91
+ }))
92
+ }
93
+ }
94
+
95
+ fn peek(&mut self) -> Option<char> {
96
+ self.chars.peek().copied()
97
+ }
98
+
99
+ fn advance(&mut self) -> Option<char> {
100
+ let c = self.chars.next()?;
101
+ self.pos += c.len_utf8();
102
+ if c == '\n' {
103
+ self.line += 1;
104
+ self.col = 1;
105
+ self.at_line_start = true;
106
+ } else {
107
+ self.col += 1;
108
+ }
109
+ Some(c)
110
+ }
111
+
112
+ fn span_start(&self) -> (usize, usize) {
113
+ (self.line, self.col)
114
+ }
115
+
116
+ fn read_indent_level(&mut self) -> usize {
117
+ let mut level = 0;
118
+ loop {
119
+ match self.peek() {
120
+ Some(' ') => {
121
+ self.advance();
122
+ level += 1;
123
+ }
124
+ Some('\t') => {
125
+ self.advance();
126
+ level += TAB_AS_LEVELS;
127
+ }
128
+ _ => break,
129
+ }
130
+ }
131
+ level.div_ceil(INDENT_WIDTH)
132
+ }
133
+
134
+ fn skip_whitespace(&mut self) {
135
+ while let Some(c) = self.peek() {
136
+ if c == ' ' || c == '\t' || c == '\r' {
137
+ self.advance();
138
+ } else if c == '\n' {
139
+ self.advance();
140
+ self.at_line_start = true;
141
+ } else {
142
+ break;
143
+ }
144
+ }
145
+ }
146
+
147
+ fn skip_line_comment(&mut self) {
148
+ while let Some(c) = self.advance() {
149
+ if c == '\n' {
150
+ break;
151
+ }
152
+ }
153
+ }
154
+
155
+ fn skip_block_comment(&mut self) -> Result<(), String> {
156
+ let mut depth = 1;
157
+ while depth > 0 {
158
+ match self.advance() {
159
+ Some('*') if self.peek() == Some('/') => {
160
+ self.advance();
161
+ depth -= 1;
162
+ }
163
+ Some('/') if self.peek() == Some('*') => {
164
+ self.advance();
165
+ depth += 1;
166
+ }
167
+ None => return Err("Unterminated block comment".to_string()),
168
+ _ => {}
169
+ }
170
+ }
171
+ Ok(())
172
+ }
173
+
174
+ fn read_number(&mut self, first: char) -> String {
175
+ let mut s = String::with_capacity(16);
176
+ s.push(first);
177
+ while let Some(c) = self.peek() {
178
+ if c.is_ascii_digit() || c == '.' {
179
+ s.push(c);
180
+ self.advance();
181
+ } else {
182
+ break;
183
+ }
184
+ }
185
+ s
186
+ }
187
+
188
+ /// Handle escape sequence, returning the unescaped character.
189
+ /// `extra_allowed` contains additional characters that can be escaped in this context.
190
+ fn handle_escape(&mut self, extra_allowed: &[char]) -> Result<char, String> {
191
+ let escaped = self.advance().ok_or("Unterminated escape")?;
192
+ match escaped {
193
+ 'n' => Ok('\n'),
194
+ 'r' => Ok('\r'),
195
+ 't' => Ok('\t'),
196
+ '\\' => Ok('\\'),
197
+ c if extra_allowed.contains(&c) => Ok(c),
198
+ _ => Err(format!("Unknown escape: \\{}", escaped)),
199
+ }
200
+ }
201
+
202
+ fn read_string(&mut self, quote: char) -> Result<String, String> {
203
+ let mut s = String::with_capacity(32);
204
+ let extra = if quote == '"' {
205
+ &['"', '\''][..]
206
+ } else {
207
+ &['\'', '"'][..]
208
+ };
209
+ loop {
210
+ match self.advance() {
211
+ None => return Err("Unterminated string".to_string()),
212
+ Some(c) if c == quote => break,
213
+ Some('\\') => s.push(self.handle_escape(extra)?),
214
+ Some(c) => s.push(c),
215
+ }
216
+ }
217
+ Ok(s)
218
+ }
219
+
220
+ fn read_ident_or_keyword(&mut self, first: char) -> String {
221
+ let mut s = String::with_capacity(16);
222
+ s.push(first);
223
+ while let Some(c) = self.peek() {
224
+ if c.is_ascii_alphanumeric() || c == '_' {
225
+ s.push(c);
226
+ self.advance();
227
+ } else {
228
+ break;
229
+ }
230
+ }
231
+ s
232
+ }
233
+
234
+ /// Read a template literal. If `is_continuation` is true, we're continuing after a `}`.
235
+ fn read_template(
236
+ &mut self,
237
+ start: (usize, usize),
238
+ is_continuation: bool,
239
+ ) -> Result<Option<Token>, String> {
240
+ let mut s = String::with_capacity(if is_continuation { 32 } else { 64 });
241
+ let extra = &['`', '$', '{'][..];
242
+
243
+ loop {
244
+ match self.advance() {
245
+ None => return Err("Unterminated template literal".to_string()),
246
+ Some('`') => {
247
+ let end = self.span_start();
248
+ let kind = if is_continuation {
249
+ TokenKind::TemplateTail
250
+ } else {
251
+ TokenKind::TemplateNoSub
252
+ };
253
+ return Ok(Some(Token {
254
+ kind,
255
+ span: Span { start, end },
256
+ literal: Some(s.into()),
257
+ }));
258
+ }
259
+ Some('$') if self.peek() == Some('{') => {
260
+ self.advance();
261
+ self.template_brace_stack.push(1);
262
+ let end = self.span_start();
263
+ let kind = if is_continuation {
264
+ TokenKind::TemplateMiddle
265
+ } else {
266
+ TokenKind::TemplateHead
267
+ };
268
+ return Ok(Some(Token {
269
+ kind,
270
+ span: Span { start, end },
271
+ literal: Some(s.into()),
272
+ }));
273
+ }
274
+ Some('\\') => s.push(self.handle_escape(extra)?),
275
+ Some(c) => s.push(c),
276
+ }
277
+ }
278
+ }
279
+
280
+ fn emit_indent_or_dedent(&mut self, level: usize) -> Option<Token> {
281
+ let top = *self.indent_stack.last().unwrap();
282
+ let start = self.span_start();
283
+
284
+ if level > top {
285
+ self.indent_stack.push(level);
286
+ Some(Token {
287
+ kind: TokenKind::Indent,
288
+ span: Span { start, end: start },
289
+ literal: None,
290
+ })
291
+ } else if level < top {
292
+ while self.indent_stack.len() > 1 && *self.indent_stack.last().unwrap() > level {
293
+ self.indent_stack.pop();
294
+ self.pending_dedents.push_back(Token {
295
+ kind: TokenKind::Dedent,
296
+ span: Span { start, end: start },
297
+ literal: None,
298
+ });
299
+ }
300
+ if *self.indent_stack.last().unwrap_or(&0) != level {
301
+ self.indent_stack.push(level);
302
+ }
303
+ self.pending_dedents.pop_front()
304
+ } else {
305
+ None
306
+ }
307
+ }
308
+
309
+ pub fn next_token(&mut self) -> Result<Option<Token>, String> {
310
+ if let Some(tok) = self.pending_dedents.pop_front() {
311
+ return Ok(Some(tok));
312
+ }
313
+
314
+ if self.jsx_after_gt {
315
+ self.jsx_after_gt = false;
316
+ if !matches!(self.peek(), Some('{') | Some('<') | None) {
317
+ let start = self.span_start();
318
+ if let Some(tok) = self.read_jsx_text(start)? {
319
+ return Ok(Some(tok));
320
+ }
321
+ }
322
+ }
323
+
324
+ if self.at_line_start {
325
+ self.at_line_start = false;
326
+ let level = self.read_indent_level();
327
+ if level > 0 || self.peek().map(|c| c != '\n').unwrap_or(false) {
328
+ if let Some(tok) = self.emit_indent_or_dedent(level) {
329
+ return Ok(Some(tok));
330
+ }
331
+ }
332
+ }
333
+
334
+ self.skip_whitespace();
335
+ if self.at_line_start {
336
+ return self.next_token();
337
+ }
338
+
339
+ let start = self.span_start();
340
+ let c = match self.advance() {
341
+ Some(c) => c,
342
+ None => {
343
+ if let Some(tok) = self.pending_dedents.pop_front() {
344
+ return Ok(Some(tok));
345
+ }
346
+ if self.indent_stack.len() > 1 {
347
+ self.indent_stack.pop();
348
+ return Ok(Some(Token {
349
+ kind: TokenKind::Dedent,
350
+ span: Span {
351
+ start: (self.line, self.col),
352
+ end: (self.line, self.col),
353
+ },
354
+ literal: None,
355
+ }));
356
+ }
357
+ return Ok(None);
358
+ }
359
+ };
360
+
361
+ let kind = match c {
362
+ '(' => TokenKind::LParen,
363
+ ')' => TokenKind::RParen,
364
+ '{' => {
365
+ if self.jsx_in_opening_tag {
366
+ if let Some(top) = self.jsx_stack.last_mut() {
367
+ top.attr_value_braces += 1;
368
+ }
369
+ } else if self.jsx_depth > 0 {
370
+ self.jsx_child_brace_depth += 1;
371
+ }
372
+ if let Some(depth) = self.template_brace_stack.last_mut() {
373
+ *depth += 1;
374
+ }
375
+ TokenKind::LBrace
376
+ }
377
+ '}' => {
378
+ let mut handled = false;
379
+ if let Some(top) = self.jsx_stack.last() {
380
+ if top.in_opener && top.attr_value_braces > 0 {
381
+ if let Some(top) = self.jsx_stack.last_mut() {
382
+ top.attr_value_braces -= 1;
383
+ }
384
+ handled = true;
385
+ }
386
+ }
387
+ if !handled && self.jsx_child_brace_depth > 0 {
388
+ self.jsx_child_brace_depth -= 1;
389
+ if self.jsx_child_brace_depth == 0 {
390
+ self.jsx_after_gt = true;
391
+ }
392
+ }
393
+ if let Some(depth) = self.template_brace_stack.last_mut() {
394
+ *depth -= 1;
395
+ if *depth == 0 {
396
+ self.template_brace_stack.pop();
397
+ return self.read_template(start, true);
398
+ }
399
+ }
400
+ TokenKind::RBrace
401
+ }
402
+ '[' => TokenKind::LBracket,
403
+ ']' => TokenKind::RBracket,
404
+ ';' => TokenKind::Semicolon,
405
+ ',' => TokenKind::Comma,
406
+ '.' => {
407
+ if self.peek() == Some('?') {
408
+ self.advance();
409
+ TokenKind::OptionalChain
410
+ } else if self.peek() == Some('.') {
411
+ self.advance();
412
+ if self.peek() == Some('.') {
413
+ self.advance();
414
+ TokenKind::Spread
415
+ } else {
416
+ return Err("Unexpected .. (use ... for rest params)".to_string());
417
+ }
418
+ } else {
419
+ TokenKind::Dot
420
+ }
421
+ }
422
+ '=' => {
423
+ if self.peek() == Some('=') {
424
+ self.advance();
425
+ if self.peek() == Some('=') {
426
+ self.advance();
427
+ TokenKind::StrictEq
428
+ } else {
429
+ TokenKind::Eq
430
+ }
431
+ } else if self.peek() == Some('>') {
432
+ self.advance();
433
+ TokenKind::Arrow
434
+ } else {
435
+ TokenKind::Assign
436
+ }
437
+ }
438
+ '!' => {
439
+ if self.peek() == Some('=') {
440
+ self.advance();
441
+ if self.peek() == Some('=') {
442
+ self.advance();
443
+ TokenKind::StrictNe
444
+ } else {
445
+ TokenKind::Ne
446
+ }
447
+ } else {
448
+ TokenKind::Not
449
+ }
450
+ }
451
+ '<' => {
452
+ if self.peek() == Some('=') {
453
+ self.advance();
454
+ TokenKind::Le
455
+ } else if self.peek() == Some('<') {
456
+ self.advance();
457
+ TokenKind::Shl
458
+ } else if self.peek() == Some('/') {
459
+ self.jsx_in_closing_tag = true;
460
+ TokenKind::Lt
461
+ } else if self.peek() == Some('>')
462
+ || self
463
+ .peek()
464
+ .map(|c| c.is_ascii_alphabetic() || c == '_')
465
+ .unwrap_or(false)
466
+ {
467
+ self.jsx_depth += 1;
468
+ self.jsx_stack.push(JsxEl {
469
+ in_opener: true,
470
+ attr_value_braces: 0,
471
+ });
472
+ self.jsx_in_opening_tag = true;
473
+ TokenKind::Lt
474
+ } else {
475
+ TokenKind::Lt
476
+ }
477
+ }
478
+ '>' => {
479
+ if self.peek() == Some('=') {
480
+ self.advance();
481
+ TokenKind::Ge
482
+ } else if self.peek() == Some('>') {
483
+ self.advance();
484
+ TokenKind::Shr
485
+ } else {
486
+ if self.jsx_in_closing_tag {
487
+ self.jsx_depth = (self.jsx_depth - 1).max(0);
488
+ self.jsx_stack.pop();
489
+ self.jsx_sync_in_opening_tag();
490
+ } else if self.jsx_in_opening_tag && self.jsx_saw_slash_before_gt {
491
+ self.jsx_depth = (self.jsx_depth - 1).max(0);
492
+ self.jsx_stack.pop();
493
+ self.jsx_sync_in_opening_tag();
494
+ } else if let Some(top) = self.jsx_stack.last_mut() {
495
+ if top.in_opener && top.attr_value_braces > 0 {
496
+ // `>` is a comparison (or shift) token inside `{ ... }`, not end of opening tag.
497
+ } else if top.in_opener && !self.jsx_saw_slash_before_gt {
498
+ top.in_opener = false;
499
+ self.jsx_after_gt = true;
500
+ self.jsx_sync_in_opening_tag();
501
+ }
502
+ }
503
+ self.jsx_in_closing_tag = false;
504
+ self.jsx_saw_slash_before_gt = false;
505
+ TokenKind::Gt
506
+ }
507
+ }
508
+ '^' => TokenKind::BitXor,
509
+ '~' => TokenKind::BitNot,
510
+ '+' => {
511
+ if self.peek() == Some('+') {
512
+ self.advance();
513
+ TokenKind::PlusPlus
514
+ } else if self.peek() == Some('=') {
515
+ self.advance();
516
+ TokenKind::PlusAssign
517
+ } else {
518
+ TokenKind::Plus
519
+ }
520
+ }
521
+ '-' => {
522
+ if self.peek() == Some('-') {
523
+ self.advance();
524
+ TokenKind::MinusMinus
525
+ } else if self.peek() == Some('=') {
526
+ self.advance();
527
+ TokenKind::MinusAssign
528
+ } else {
529
+ TokenKind::Minus
530
+ }
531
+ }
532
+ '*' => {
533
+ if self.peek() == Some('*') {
534
+ self.advance();
535
+ TokenKind::StarStar
536
+ } else if self.peek() == Some('=') {
537
+ self.advance();
538
+ TokenKind::StarAssign
539
+ } else {
540
+ TokenKind::Star
541
+ }
542
+ }
543
+ '/' => {
544
+ if self.peek() == Some('/') {
545
+ self.advance();
546
+ self.skip_line_comment();
547
+ // `skip_line_comment` consumes the newline via `advance()`, which sets
548
+ // `at_line_start` before we would normally run `skip_whitespace()`. Without
549
+ // stripping the next line's leading spaces here, `read_indent_level` would see
550
+ // physical indentation and emit a spurious `Indent` (breaks e.g. object
551
+ // literals with trailing `//` comments). Newlines handled in `skip_whitespace`
552
+ // eat those spaces before the indent pass; match that behavior.
553
+ self.skip_whitespace();
554
+ return self.next_token();
555
+ } else if self.peek() == Some('*') {
556
+ self.advance();
557
+ self.skip_block_comment()?;
558
+ return self.next_token();
559
+ } else if self.peek() == Some('=') {
560
+ self.advance();
561
+ TokenKind::SlashAssign
562
+ } else {
563
+ if self.jsx_in_opening_tag {
564
+ self.jsx_saw_slash_before_gt = true;
565
+ }
566
+ TokenKind::Slash
567
+ }
568
+ }
569
+ '%' => {
570
+ if self.peek() == Some('=') {
571
+ self.advance();
572
+ TokenKind::PercentAssign
573
+ } else {
574
+ TokenKind::Percent
575
+ }
576
+ }
577
+ '&' => {
578
+ if self.peek() == Some('&') {
579
+ self.advance();
580
+ if self.peek() == Some('=') {
581
+ self.advance();
582
+ TokenKind::AndAndAssign
583
+ } else {
584
+ TokenKind::And
585
+ }
586
+ } else {
587
+ TokenKind::BitAnd
588
+ }
589
+ }
590
+ '|' => {
591
+ if self.peek() == Some('|') {
592
+ self.advance();
593
+ if self.peek() == Some('=') {
594
+ self.advance();
595
+ TokenKind::OrOrAssign
596
+ } else {
597
+ TokenKind::Or
598
+ }
599
+ } else {
600
+ TokenKind::BitOr
601
+ }
602
+ }
603
+ '?' => {
604
+ if self.peek() == Some('?') {
605
+ self.advance();
606
+ if self.peek() == Some('=') {
607
+ self.advance();
608
+ TokenKind::NullishAssign
609
+ } else {
610
+ TokenKind::NullishCoalesce
611
+ }
612
+ } else if self.peek() == Some('.') {
613
+ self.advance();
614
+ TokenKind::OptionalChain
615
+ } else {
616
+ TokenKind::Question
617
+ }
618
+ }
619
+ ':' => TokenKind::Colon,
620
+ '"' | '\'' => {
621
+ let s = self.read_string(c)?;
622
+ let end = self.span_start();
623
+ return Ok(Some(Token {
624
+ kind: TokenKind::String,
625
+ span: Span { start, end },
626
+ literal: Some(s.into()),
627
+ }));
628
+ }
629
+ '`' => return self.read_template(start, false),
630
+ '0'..='9' => {
631
+ let num = self.read_number(c);
632
+ let end = self.span_start();
633
+ return Ok(Some(Token {
634
+ kind: TokenKind::Number,
635
+ span: Span { start, end },
636
+ literal: Some(num.into()),
637
+ }));
638
+ }
639
+ 'a'..='z' | 'A'..='Z' | '_' => {
640
+ let ident = self.read_ident_or_keyword(c);
641
+ let end = self.span_start();
642
+ let kind = TokenKind::keyword_or_ident(&ident);
643
+ return Ok(Some(Token {
644
+ kind,
645
+ span: Span { start, end },
646
+ // Spelling is useful for keywords too (e.g. object keys, type names like `type`).
647
+ literal: Some(ident.into()),
648
+ }));
649
+ }
650
+ '\n' => {
651
+ self.at_line_start = true;
652
+ return self.next_token();
653
+ }
654
+ _ => return Err(format!("Unexpected character: {:?}", c)),
655
+ };
656
+
657
+ let end = self.span_start();
658
+ Ok(Some(Token {
659
+ kind,
660
+ span: Span { start, end },
661
+ literal: None,
662
+ }))
663
+ }
664
+ }
665
+
666
+ impl<'a> Iterator for Lexer<'a> {
667
+ type Item = Result<Token, String>;
668
+
669
+ fn next(&mut self) -> Option<Self::Item> {
670
+ match self.next_token() {
671
+ Ok(Some(t)) => Some(Ok(t)),
672
+ Ok(None) => None,
673
+ Err(e) => Some(Err(e)),
674
+ }
675
+ }
676
+ }
677
+
678
+ #[cfg(test)]
679
+ mod tests {
680
+ use super::*;
681
+
682
+ #[test]
683
+ fn test_string_literal() {
684
+ let tokens: Vec<_> = Lexer::new(r#""H""#).collect();
685
+ let tokens: Result<Vec<_>, _> = tokens.into_iter().collect();
686
+ let tokens = tokens.unwrap();
687
+ assert_eq!(tokens.len(), 1);
688
+ assert_eq!(tokens[0].kind, TokenKind::String);
689
+ assert_eq!(tokens[0].literal.as_deref(), Some("H"));
690
+ }
691
+
692
+ #[test]
693
+ fn test_print_string() {
694
+ let tokens: Vec<_> = Lexer::new(r#"print("H")"#).collect();
695
+ let tokens: Result<Vec<_>, _> = tokens.into_iter().collect();
696
+ let tokens = tokens.unwrap();
697
+ let string_tok = tokens.iter().find(|t| t.kind == TokenKind::String).unwrap();
698
+ assert_eq!(string_tok.literal.as_deref(), Some("H"));
699
+ }
700
+
701
+ #[test]
702
+ fn line_comment_does_not_emit_spurious_indent_before_next_line() {
703
+ let with_comment = "fn f() {\n return {\n a: 1, // c\n b: 2\n }\n}\n";
704
+ let tokens: Vec<_> = Lexer::new(with_comment)
705
+ .collect::<Result<Vec<_>, _>>()
706
+ .unwrap();
707
+ assert!(
708
+ !tokens.iter().any(|t| t.kind == TokenKind::Indent),
709
+ "unexpected Indent after line comment: {:?}",
710
+ tokens
711
+ .iter()
712
+ .map(|t| format!("{:?}", t.kind))
713
+ .collect::<Vec<_>>()
714
+ );
715
+ }
716
+ }