@tishlang/tish 1.9.1 → 1.10.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 (79) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +8 -6
  3. package/crates/js_to_tish/src/transform/stmt.rs +12 -13
  4. package/crates/tish/Cargo.toml +1 -1
  5. package/crates/tish/src/cargo_native_registry.rs +4 -1
  6. package/crates/tish/src/main.rs +11 -8
  7. package/crates/tish/tests/integration_test.rs +145 -7
  8. package/crates/tish_ast/src/ast.rs +3 -9
  9. package/crates/tish_build_utils/src/lib.rs +43 -15
  10. package/crates/tish_builtins/src/array.rs +2 -3
  11. package/crates/tish_builtins/src/construct.rs +15 -28
  12. package/crates/tish_builtins/src/globals.rs +18 -16
  13. package/crates/tish_builtins/src/helpers.rs +1 -4
  14. package/crates/tish_builtins/src/lib.rs +1 -0
  15. package/crates/tish_builtins/src/object.rs +10 -10
  16. package/crates/tish_builtins/src/string.rs +1 -3
  17. package/crates/tish_builtins/src/symbol.rs +83 -0
  18. package/crates/tish_compile/src/codegen.rs +123 -138
  19. package/crates/tish_compile/src/lib.rs +25 -3
  20. package/crates/tish_compile/src/resolve.rs +6 -3
  21. package/crates/tish_compile/src/types.rs +6 -6
  22. package/crates/tish_compile_js/src/codegen.rs +50 -29
  23. package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
  24. package/crates/tish_core/src/console_style.rs +9 -0
  25. package/crates/tish_core/src/json.rs +17 -7
  26. package/crates/tish_core/src/macros.rs +2 -2
  27. package/crates/tish_core/src/value.rs +192 -4
  28. package/crates/tish_cranelift_runtime/Cargo.toml +4 -0
  29. package/crates/tish_eval/src/eval.rs +135 -73
  30. package/crates/tish_eval/src/http.rs +18 -12
  31. package/crates/tish_eval/src/lib.rs +29 -0
  32. package/crates/tish_eval/src/regex.rs +1 -1
  33. package/crates/tish_eval/src/value.rs +89 -4
  34. package/crates/tish_eval/src/value_convert.rs +30 -8
  35. package/crates/tish_fmt/src/lib.rs +4 -1
  36. package/crates/tish_lexer/src/lib.rs +7 -2
  37. package/crates/tish_llvm/src/lib.rs +2 -2
  38. package/crates/tish_lsp/src/builtin_goto.rs +111 -10
  39. package/crates/tish_lsp/src/import_goto.rs +35 -22
  40. package/crates/tish_lsp/src/main.rs +118 -85
  41. package/crates/tish_native/src/build.rs +187 -10
  42. package/crates/tish_native/src/lib.rs +92 -8
  43. package/crates/tish_parser/src/lib.rs +77 -0
  44. package/crates/tish_parser/src/parser.rs +71 -74
  45. package/crates/tish_pg/src/error.rs +1 -1
  46. package/crates/tish_pg/src/lib.rs +61 -73
  47. package/crates/tish_resolve/src/lib.rs +283 -158
  48. package/crates/tish_resolve/src/pos.rs +10 -2
  49. package/crates/tish_runtime/Cargo.toml +3 -0
  50. package/crates/tish_runtime/src/http.rs +39 -39
  51. package/crates/tish_runtime/src/http_fetch.rs +12 -12
  52. package/crates/tish_runtime/src/lib.rs +26 -43
  53. package/crates/tish_runtime/src/native_promise.rs +0 -11
  54. package/crates/tish_runtime/src/promise.rs +14 -1
  55. package/crates/tish_runtime/src/promise_io.rs +1 -4
  56. package/crates/tish_runtime/src/ws.rs +40 -27
  57. package/crates/tish_runtime/tests/fetch_readable_stream.rs +10 -8
  58. package/crates/tish_ui/src/jsx.rs +6 -4
  59. package/crates/tish_ui/src/lib.rs +2 -2
  60. package/crates/tish_ui/src/runtime/hooks.rs +5 -15
  61. package/crates/tish_ui/src/runtime/mod.rs +16 -17
  62. package/crates/tish_vm/Cargo.toml +2 -0
  63. package/crates/tish_vm/src/vm.rs +218 -153
  64. package/crates/tish_wasm/src/lib.rs +33 -7
  65. package/crates/tish_wasm_runtime/Cargo.toml +4 -1
  66. package/crates/tish_wasm_runtime/src/lib.rs +2 -1
  67. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  68. package/crates/tishlang_cargo_bindgen/src/discover.rs +10 -5
  69. package/crates/tishlang_cargo_bindgen/src/infer.rs +18 -8
  70. package/crates/tishlang_cargo_bindgen/src/lib.rs +25 -26
  71. package/crates/tishlang_cargo_bindgen/src/main.rs +41 -38
  72. package/crates/tishlang_cargo_bindgen/src/metadata.rs +4 -1
  73. package/justfile +3 -3
  74. package/package.json +1 -1
  75. package/platform/darwin-arm64/tish +0 -0
  76. package/platform/darwin-x64/tish +0 -0
  77. package/platform/linux-arm64/tish +0 -0
  78. package/platform/linux-x64/tish +0 -0
  79. package/platform/win32-x64/tish.exe +0 -0
@@ -14,6 +14,22 @@ mod build;
14
14
  use std::path::Path;
15
15
  use tishlang_ast::Program;
16
16
 
17
+ /// Features for the embeddable VM (`cranelift` / `llvm` backends): merge CLI capability flags
18
+ /// (same set as `tish run` / `tish build --target native`) with features inferred from
19
+ /// `import … from 'tish:…'`. Programs that use globals only (`Promise`, `fetch`, `setTimeout`)
20
+ /// still need the former so [`tishlang_cranelift_runtime`] links `tishlang_vm` with matching gates.
21
+ fn merged_embed_runtime_features(cli: &[String], program: &Program) -> Vec<String> {
22
+ let mut out = cli.to_vec();
23
+ for f in tishlang_compile::extract_native_import_features(program) {
24
+ if !out.contains(&f) {
25
+ out.push(f);
26
+ }
27
+ }
28
+ out.sort();
29
+ out.dedup();
30
+ out
31
+ }
32
+
17
33
  /// Error from native compilation.
18
34
  #[derive(Debug)]
19
35
  pub struct NativeError {
@@ -93,8 +109,8 @@ pub fn compile_to_native(
93
109
  let prog = tishlang_compile::merge_modules(modules)
94
110
  .map(|m| m.program)
95
111
  .map_err(|e| NativeError {
96
- message: e.to_string(),
97
- })?;
112
+ message: e.to_string(),
113
+ })?;
98
114
  if optimize {
99
115
  tishlang_opt::optimize(&prog)
100
116
  } else {
@@ -118,7 +134,7 @@ pub fn compile_to_native(
118
134
  })?
119
135
  };
120
136
 
121
- let cranelift_features = tishlang_compile::extract_native_import_features(&program);
137
+ let cranelift_features = merged_embed_runtime_features(features, &program);
122
138
  tishlang_cranelift::compile_chunk_to_native(&chunk, output_path, &cranelift_features)
123
139
  .map_err(|e| NativeError {
124
140
  message: e.to_string(),
@@ -138,8 +154,8 @@ pub fn compile_to_native(
138
154
  let prog = tishlang_compile::merge_modules(modules)
139
155
  .map(|m| m.program)
140
156
  .map_err(|e| NativeError {
141
- message: e.to_string(),
142
- })?;
157
+ message: e.to_string(),
158
+ })?;
143
159
  if optimize {
144
160
  tishlang_opt::optimize(&prog)
145
161
  } else {
@@ -160,7 +176,7 @@ pub fn compile_to_native(
160
176
  message: e.to_string(),
161
177
  })?
162
178
  };
163
- let llvm_features = tishlang_compile::extract_native_import_features(&program);
179
+ let llvm_features = merged_embed_runtime_features(features, &program);
164
180
  tishlang_llvm::compile_chunk_to_native(&chunk, output_path, &llvm_features)
165
181
  .map_err(|e| NativeError { message: e.message })
166
182
  }
@@ -249,7 +265,7 @@ pub fn compile_program_to_native(
249
265
  message: e.to_string(),
250
266
  })?
251
267
  };
252
- let cranelift_features = tishlang_compile::extract_native_import_features(&program);
268
+ let cranelift_features = merged_embed_runtime_features(features, &program);
253
269
  tishlang_cranelift::compile_chunk_to_native(&chunk, output_path, &cranelift_features)
254
270
  .map_err(|e| NativeError {
255
271
  message: e.to_string(),
@@ -275,13 +291,81 @@ pub fn compile_program_to_native(
275
291
  message: e.to_string(),
276
292
  })?
277
293
  };
278
- let llvm_features = tishlang_compile::extract_native_import_features(&program);
294
+ let llvm_features = merged_embed_runtime_features(features, &program);
279
295
  tishlang_llvm::compile_chunk_to_native(&chunk, output_path, &llvm_features)
280
296
  .map_err(|e| NativeError { message: e.message })
281
297
  }
282
298
  }
283
299
  }
284
300
 
301
+ /// Compile multiple entry `.tish` files to native binaries in **one** nested Cargo build.
302
+ ///
303
+ /// Intended for integration tests and batch tooling; keeps production [`compile_to_native`] behavior
304
+ /// unchanged when `TISH_FAST_NATIVE_BUILD` is unset.
305
+ pub fn compile_many_to_native(
306
+ entries: &[(&Path, &Path)],
307
+ project_root: Option<&Path>,
308
+ features: &[String],
309
+ optimize: bool,
310
+ ) -> Result<(), NativeError> {
311
+ let mut bins: Vec<(String, String, Option<String>)> = Vec::with_capacity(entries.len());
312
+ let mut merged_native_modules: Vec<tishlang_compile::ResolvedNativeModule> = Vec::new();
313
+ let mut merged_features: Vec<String> = features.to_vec();
314
+ let mut merged_extra_deps = String::new();
315
+ let mut needs_tokio = false;
316
+ let mut needs_ui = false;
317
+
318
+ for (entry_path, _) in entries {
319
+ let (rust_code, native_modules, effective_features, native_build) =
320
+ tishlang_compile::compile_project_full(entry_path, project_root, features, optimize)
321
+ .map_err(|e| NativeError {
322
+ message: e.to_string(),
323
+ })?;
324
+ let stem = entry_path
325
+ .file_stem()
326
+ .and_then(|s| s.to_str())
327
+ .ok_or_else(|| NativeError {
328
+ message: format!("invalid entry path: {}", entry_path.display()),
329
+ })?
330
+ .to_string();
331
+
332
+ for f in &effective_features {
333
+ if !merged_features.contains(f) {
334
+ merged_features.push(f.clone());
335
+ }
336
+ }
337
+ for m in native_modules {
338
+ let dup = merged_native_modules
339
+ .iter()
340
+ .any(|x| x.package_name == m.package_name && x.crate_path == m.crate_path);
341
+ if !dup {
342
+ merged_native_modules.push(m);
343
+ }
344
+ }
345
+ let extra = native_build.rust_dependencies_toml.trim();
346
+ if !extra.is_empty() {
347
+ merged_extra_deps.push_str(extra);
348
+ merged_extra_deps.push('\n');
349
+ }
350
+ needs_tokio |= rust_code.contains("#[tokio::main]");
351
+ needs_ui |= rust_code.contains("tishlang_ui");
352
+ bins.push((stem, rust_code, native_build.generated_native_rs));
353
+ }
354
+
355
+ let merged_extra = merged_extra_deps.trim();
356
+ crate::build::build_many_via_cargo(
357
+ bins,
358
+ merged_native_modules,
359
+ &merged_features,
360
+ merged_extra,
361
+ needs_tokio,
362
+ needs_ui,
363
+ entries,
364
+ project_root,
365
+ )
366
+ .map_err(|e| NativeError { message: e })
367
+ }
368
+
285
369
  enum Backend {
286
370
  Rust,
287
371
  Cranelift,
@@ -252,4 +252,81 @@ mod tests {
252
252
  parse(SRC).expect("stdlib/builtins.d.tish should parse");
253
253
  }
254
254
 
255
+ #[test]
256
+ fn for_empty_head_parses() {
257
+ let src = r#"fn f() {
258
+ for (;;)
259
+ const x = 1
260
+ }"#;
261
+ let program = parse(src).expect("for (;;)");
262
+ let body = match &program.statements[0] {
263
+ Statement::FunDecl { body, .. } => body,
264
+ _ => panic!("expected fn"),
265
+ };
266
+ let stmts = match body.as_ref() {
267
+ Statement::Block { statements, .. } => statements,
268
+ _ => panic!("expected block body"),
269
+ };
270
+ assert!(
271
+ matches!(
272
+ stmts.iter().find(|s| matches!(s, Statement::For { .. })),
273
+ Some(Statement::For {
274
+ init: None,
275
+ cond: None,
276
+ update: None,
277
+ ..
278
+ })
279
+ ),
280
+ "expected for (;;)"
281
+ );
282
+ }
283
+
284
+ #[test]
285
+ fn brace_function_body_does_not_nest_block_around_first_let() {
286
+ let src = "fn h() {\n let a = 1\n let b = 2\n}\n";
287
+ let program = parse(src).expect("parse");
288
+ let body = match &program.statements[0] {
289
+ Statement::FunDecl { body, .. } => body,
290
+ _ => panic!("expected fn"),
291
+ };
292
+ let stmts = match body.as_ref() {
293
+ Statement::Block { statements, .. } => statements,
294
+ _ => panic!("expected block body"),
295
+ };
296
+ assert_eq!(
297
+ stmts.len(),
298
+ 2,
299
+ "expected two top-level lets in fn body, not Block(let) + let — got {stmts:?}"
300
+ );
301
+ assert!(matches!(stmts[0], Statement::VarDecl { .. }));
302
+ assert!(matches!(stmts[1], Statement::VarDecl { .. }));
303
+ }
304
+
305
+ #[test]
306
+ fn member_access_allows_type_property_name() {
307
+ let src = "fn f() {\n const label = 0\n label.type = \"button\"\n}\n";
308
+ parse(src).expect("label.type should parse: `type` is a keyword but valid after `.`");
309
+ }
310
+
311
+ #[test]
312
+ fn brace_block_stmt_then_const_then_if_are_siblings() {
313
+ let src = "fn g() {\n f()\n const x = 1\n if (x) {\n f()\n }\n}\n";
314
+ let program = parse(src).expect("parse");
315
+ let body = match &program.statements[0] {
316
+ Statement::FunDecl { body, .. } => body,
317
+ _ => panic!("expected fn"),
318
+ };
319
+ let stmts = match body.as_ref() {
320
+ Statement::Block { statements, .. } => statements,
321
+ _ => panic!("expected block body"),
322
+ };
323
+ assert_eq!(
324
+ stmts.len(),
325
+ 3,
326
+ "expected expr; const; if as siblings — got {stmts:?}"
327
+ );
328
+ assert!(matches!(stmts[0], Statement::ExprStmt { .. }));
329
+ assert!(matches!(stmts[1], Statement::VarDecl { .. }));
330
+ assert!(matches!(stmts[2], Statement::If { .. }));
331
+ }
255
332
  }
@@ -99,6 +99,18 @@ impl<'a> Parser<'a> {
99
99
  }
100
100
  }
101
101
 
102
+ /// After `.` / `?.`, allow `type` as a member name (`TokenKind::Type`); see `docs/js-emit-philosophy.md`.
103
+ fn expect_ident_or_type_member_name(&mut self) -> Result<&Token, String> {
104
+ match self.peek_kind() {
105
+ Some(TokenKind::Ident) => self.expect(TokenKind::Ident),
106
+ Some(TokenKind::Type) => self.expect(TokenKind::Type),
107
+ other => Err(format!(
108
+ "Expected property name after `.` or `?.`, got {:?}",
109
+ other
110
+ )),
111
+ }
112
+ }
113
+
102
114
  fn span_end(&self, start: (usize, usize)) -> Span {
103
115
  let end = self.peek().map(|t| t.span.start).unwrap_or(start);
104
116
  Span { start, end }
@@ -194,8 +206,17 @@ impl<'a> Parser<'a> {
194
206
  fn parse_block(&mut self) -> Result<Statement, String> {
195
207
  let span_start = self.peek().ok_or("Unexpected EOF")?.span.start;
196
208
 
197
- if matches!(self.peek_kind(), Some(TokenKind::LBrace)) {
209
+ let opened_with_brace = matches!(self.peek_kind(), Some(TokenKind::LBrace));
210
+ if opened_with_brace {
198
211
  self.advance(); // {
212
+ // After `{`, the lexer often emits `Indent` for the first indented line of the body.
213
+ // `parse_statement` treats a leading `Indent` as starting a *nested* indent-block, so
214
+ // without consuming this token we get `Block { Block { let ... } ; ... }` and the first
215
+ // `let`/`const` is scoped too narrowly (JS ReferenceError). This indent is layout for
216
+ // *this* brace block, not an inner block.
217
+ if matches!(self.peek_kind(), Some(TokenKind::Indent)) {
218
+ self.advance();
219
+ }
199
220
  } else if matches!(self.peek_kind(), Some(TokenKind::Indent)) {
200
221
  self.advance(); // Indent
201
222
  }
@@ -270,10 +291,7 @@ impl<'a> Parser<'a> {
270
291
  start: name_tok.span.start,
271
292
  end: name_tok.span.end,
272
293
  };
273
- let name = name_tok
274
- .literal
275
- .clone()
276
- .ok_or("Expected identifier")?;
294
+ let name = name_tok.literal.clone().ok_or("Expected identifier")?;
277
295
 
278
296
  // Optional type annotation: `: Type`
279
297
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
@@ -327,10 +345,7 @@ impl<'a> Parser<'a> {
327
345
  start: name_tok.span.start,
328
346
  end: name_tok.span.end,
329
347
  };
330
- let name = name_tok
331
- .literal
332
- .clone()
333
- .ok_or("Expected identifier")?;
348
+ let name = name_tok.literal.clone().ok_or("Expected identifier")?;
334
349
  elements.push(Some(DestructElement::Rest(name, name_span)));
335
350
  break;
336
351
  }
@@ -347,10 +362,7 @@ impl<'a> Parser<'a> {
347
362
  start: name_tok.span.start,
348
363
  end: name_tok.span.end,
349
364
  };
350
- let name = name_tok
351
- .literal
352
- .clone()
353
- .ok_or("Expected identifier")?;
365
+ let name = name_tok.literal.clone().ok_or("Expected identifier")?;
354
366
  DestructElement::Ident(name, name_span)
355
367
  }
356
368
  _ => return Err("Expected identifier or pattern in destructuring".to_string()),
@@ -378,10 +390,7 @@ impl<'a> Parser<'a> {
378
390
  start: key_tok.span.start,
379
391
  end: key_tok.span.end,
380
392
  };
381
- let key = key_tok
382
- .literal
383
- .clone()
384
- .ok_or("Expected identifier")?;
393
+ let key = key_tok.literal.clone().ok_or("Expected identifier")?;
385
394
 
386
395
  let value = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
387
396
  self.advance();
@@ -397,10 +406,7 @@ impl<'a> Parser<'a> {
397
406
  start: name_tok.span.start,
398
407
  end: name_tok.span.end,
399
408
  };
400
- let name = name_tok
401
- .literal
402
- .clone()
403
- .ok_or("Expected identifier")?;
409
+ let name = name_tok.literal.clone().ok_or("Expected identifier")?;
404
410
  DestructElement::Ident(name, name_span)
405
411
  }
406
412
  _ => return Err("Expected identifier or pattern after ':'".to_string()),
@@ -453,10 +459,7 @@ impl<'a> Parser<'a> {
453
459
  start: param_tok.span.start,
454
460
  end: param_tok.span.end,
455
461
  };
456
- let param_name = param_tok
457
- .literal
458
- .clone()
459
- .ok_or("Expected param name")?;
462
+ let param_name = param_tok.literal.clone().ok_or("Expected param name")?;
460
463
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
461
464
  self.advance();
462
465
  Some(self.parse_type_annotation()?)
@@ -528,11 +531,27 @@ impl<'a> Parser<'a> {
528
531
  self.advance(); // {
529
532
  let mut props = Vec::new();
530
533
  while !matches!(self.peek_kind(), Some(TokenKind::RBrace)) {
531
- let key = self
532
- .expect(TokenKind::Ident)?
533
- .literal
534
- .clone()
535
- .ok_or("Expected property name")?;
534
+ // `for` is a keyword but a common method name (`Symbol.for`); allow it here.
535
+ let key: Arc<str> = match self.peek_kind() {
536
+ Some(TokenKind::Ident) => {
537
+ let tok = self.expect(TokenKind::Ident)?;
538
+ Arc::from(
539
+ tok.literal
540
+ .as_deref()
541
+ .ok_or("Expected property name")?,
542
+ )
543
+ }
544
+ Some(TokenKind::For) => {
545
+ self.advance();
546
+ Arc::from("for")
547
+ }
548
+ _ => {
549
+ return Err(format!(
550
+ "Expected Ident or `for` as object type property name, got {:?}",
551
+ self.peek_kind()
552
+ ));
553
+ }
554
+ };
536
555
  self.expect(TokenKind::Colon)?;
537
556
  let typ = self.parse_type_annotation()?;
538
557
  props.push((key, typ));
@@ -580,10 +599,7 @@ impl<'a> Parser<'a> {
580
599
  start: name_tok.span.start,
581
600
  end: name_tok.span.end,
582
601
  };
583
- let name = name_tok
584
- .literal
585
- .clone()
586
- .ok_or("Expected function name")?;
602
+ let name = name_tok.literal.clone().ok_or("Expected function name")?;
587
603
  self.expect(TokenKind::LParen)?;
588
604
  let mut params = Vec::with_capacity(4);
589
605
  let mut rest_param = None;
@@ -595,10 +611,7 @@ impl<'a> Parser<'a> {
595
611
  start: rest_tok.span.start,
596
612
  end: rest_tok.span.end,
597
613
  };
598
- let param_name = rest_tok
599
- .literal
600
- .clone()
601
- .ok_or("Expected rest param name")?;
614
+ let param_name = rest_tok.literal.clone().ok_or("Expected rest param name")?;
602
615
  // Optional type annotation for rest param
603
616
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
604
617
  self.advance();
@@ -681,10 +694,7 @@ impl<'a> Parser<'a> {
681
694
  start: name_tok.span.start,
682
695
  end: name_tok.span.end,
683
696
  };
684
- let name = name_tok
685
- .literal
686
- .clone()
687
- .ok_or("Expected type alias name")?;
697
+ let name = name_tok.literal.clone().ok_or("Expected type alias name")?;
688
698
  self.expect(TokenKind::Assign)?;
689
699
  let ty = self.parse_type_annotation()?;
690
700
  Ok(Statement::TypeAlias {
@@ -726,10 +736,7 @@ impl<'a> Parser<'a> {
726
736
  start: name_tok.span.start,
727
737
  end: name_tok.span.end,
728
738
  };
729
- let name = name_tok
730
- .literal
731
- .clone()
732
- .ok_or("Expected identifier")?;
739
+ let name = name_tok.literal.clone().ok_or("Expected identifier")?;
733
740
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
734
741
  self.advance();
735
742
  Some(self.parse_type_annotation()?)
@@ -759,10 +766,7 @@ impl<'a> Parser<'a> {
759
766
  start: name_tok.span.start,
760
767
  end: name_tok.span.end,
761
768
  };
762
- let name = name_tok
763
- .literal
764
- .clone()
765
- .ok_or("Expected function name")?;
769
+ let name = name_tok.literal.clone().ok_or("Expected function name")?;
766
770
  self.expect(TokenKind::LParen)?;
767
771
  let mut params = Vec::with_capacity(4);
768
772
  let mut rest_param = None;
@@ -774,10 +778,7 @@ impl<'a> Parser<'a> {
774
778
  start: rest_tok.span.start,
775
779
  end: rest_tok.span.end,
776
780
  };
777
- let param_name = rest_tok
778
- .literal
779
- .clone()
780
- .ok_or("Expected rest param name")?;
781
+ let param_name = rest_tok.literal.clone().ok_or("Expected rest param name")?;
781
782
  let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
782
783
  self.advance();
783
784
  Some(self.parse_type_annotation()?)
@@ -872,10 +873,7 @@ impl<'a> Parser<'a> {
872
873
  start: for_name_tok.span.start,
873
874
  end: for_name_tok.span.end,
874
875
  };
875
- let name = for_name_tok
876
- .literal
877
- .clone()
878
- .ok_or("Expected identifier")?;
876
+ let name = for_name_tok.literal.clone().ok_or("Expected identifier")?;
879
877
  if matches!(self.peek_kind(), Some(TokenKind::Of)) {
880
878
  self.advance();
881
879
  let iterable = self.parse_expr()?;
@@ -938,6 +936,11 @@ impl<'a> Parser<'a> {
938
936
  self.expect(TokenKind::Semicolon)?;
939
937
  Some(c)
940
938
  };
939
+ // `for (init; ; update)` — when the condition is empty we matched `;` above but did not
940
+ // consume it; skip it so `update` / `)` parse correctly (e.g. `for (;;)`).
941
+ if cond.is_none() && matches!(self.peek_kind(), Some(TokenKind::Semicolon)) {
942
+ self.advance();
943
+ }
941
944
  let update = if matches!(self.peek_kind(), Some(TokenKind::RParen)) {
942
945
  None
943
946
  } else {
@@ -1160,10 +1163,7 @@ impl<'a> Parser<'a> {
1160
1163
  start: def_tok.span.start,
1161
1164
  end: def_tok.span.end,
1162
1165
  };
1163
- let name = def_tok
1164
- .literal
1165
- .clone()
1166
- .ok_or("Expected identifier")?;
1166
+ let name = def_tok.literal.clone().ok_or("Expected identifier")?;
1167
1167
  vec![ImportSpecifier::Default { name, name_span }]
1168
1168
  } else {
1169
1169
  return Err("Expected { }, * as name, or default import".to_string());
@@ -1245,7 +1245,9 @@ impl<'a> Parser<'a> {
1245
1245
  // Member assignment: obj.prop = val
1246
1246
  if let Expr::Member {
1247
1247
  object,
1248
- prop: MemberProp::Name { name: prop_name, .. },
1248
+ prop: MemberProp::Name {
1249
+ name: prop_name, ..
1250
+ },
1249
1251
  ..
1250
1252
  } = &left
1251
1253
  {
@@ -1483,11 +1485,8 @@ impl<'a> Parser<'a> {
1483
1485
  TokenKind::Dot | TokenKind::OptionalChain => {
1484
1486
  let optional = kind == TokenKind::OptionalChain;
1485
1487
  self.advance();
1486
- let prop_tok = self.expect(TokenKind::Ident)?;
1487
- let prop = prop_tok
1488
- .literal
1489
- .clone()
1490
- .ok_or("Expected property name")?;
1488
+ let prop_tok = self.expect_ident_or_type_member_name()?;
1489
+ let prop = prop_tok.literal.clone().ok_or("Expected property name")?;
1491
1490
  let prop_span = Span {
1492
1491
  start: prop_tok.span.start,
1493
1492
  end: prop_tok.span.end,
@@ -1603,11 +1602,8 @@ impl<'a> Parser<'a> {
1603
1602
  TokenKind::Dot | TokenKind::OptionalChain => {
1604
1603
  let optional = kind == TokenKind::OptionalChain;
1605
1604
  self.advance();
1606
- let prop_tok = self.expect(TokenKind::Ident)?;
1607
- let prop = prop_tok
1608
- .literal
1609
- .clone()
1610
- .ok_or("Expected property name")?;
1605
+ let prop_tok = self.expect_ident_or_type_member_name()?;
1606
+ let prop = prop_tok.literal.clone().ok_or("Expected property name")?;
1611
1607
  let prop_span = Span {
1612
1608
  start: prop_tok.span.start,
1613
1609
  end: prop_tok.span.end,
@@ -1997,7 +1993,8 @@ impl<'a> Parser<'a> {
1997
1993
  self.expect(TokenKind::RBrace)?; // }
1998
1994
  props.push(JsxProp::Spread(expr));
1999
1995
  }
2000
- Some(TokenKind::Ident) => {
1996
+ // `type` is `TokenKind::Type` but valid as a JSX attr name; see docs/js-emit-philosophy.md.
1997
+ Some(TokenKind::Ident) | Some(TokenKind::Type) => {
2001
1998
  let name_tok = self.advance().unwrap();
2002
1999
  let name = name_tok.literal.clone().ok_or("Expected attr name")?;
2003
2000
  if matches!(self.peek_kind(), Some(TokenKind::Assign)) {
@@ -1,5 +1,5 @@
1
- use std::fmt::Write as _;
2
1
  use std::error::Error as StdError;
2
+ use std::fmt::Write as _;
3
3
 
4
4
  use deadpool::managed::PoolError;
5
5
  use thiserror::Error;