@tishlang/tish 1.9.0 → 1.9.2

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.
package/bin/tish CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.9.0"
3
+ version = "1.9.2"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -34,6 +34,29 @@ impl Codegen {
34
34
  }
35
35
  }
36
36
 
37
+ /// ECMAScript does not allow `if (c) const x = 1` / `while (c) let y = 2` without a block.
38
+ fn stmt_needs_braces_in_js_control_head(stmt: &Statement) -> bool {
39
+ matches!(
40
+ stmt,
41
+ Statement::VarDecl { .. } | Statement::VarDeclDestructure { .. }
42
+ )
43
+ }
44
+
45
+ fn emit_js_control_body(&mut self, body: &Statement) -> Result<(), CompileError> {
46
+ if Self::stmt_needs_braces_in_js_control_head(body) {
47
+ self.writeln("{");
48
+ self.indent += 1;
49
+ self.emit_statement(body)?;
50
+ self.indent -= 1;
51
+ self.writeln("}");
52
+ } else {
53
+ self.indent += 1;
54
+ self.emit_statement(body)?;
55
+ self.indent -= 1;
56
+ }
57
+ Ok(())
58
+ }
59
+
37
60
  fn indent_str(&self) -> String {
38
61
  " ".repeat(self.indent)
39
62
  }
@@ -155,22 +178,16 @@ impl Codegen {
155
178
  } => {
156
179
  let c = self.emit_expr(cond)?;
157
180
  self.writeln(&format!("if ({})", c));
158
- self.indent += 1;
159
- self.emit_statement(then_branch)?;
160
- self.indent -= 1;
181
+ self.emit_js_control_body(then_branch)?;
161
182
  if let Some(eb) = else_branch {
162
183
  self.writeln("else");
163
- self.indent += 1;
164
- self.emit_statement(eb)?;
165
- self.indent -= 1;
184
+ self.emit_js_control_body(eb)?;
166
185
  }
167
186
  }
168
187
  Statement::While { cond, body, .. } => {
169
188
  let c = self.emit_expr(cond)?;
170
189
  self.writeln(&format!("while ({})", c));
171
- self.indent += 1;
172
- self.emit_statement(body)?;
173
- self.indent -= 1;
190
+ self.emit_js_control_body(body)?;
174
191
  }
175
192
  Statement::For {
176
193
  init,
@@ -179,7 +196,10 @@ impl Codegen {
179
196
  body,
180
197
  ..
181
198
  } => {
182
- self.write("for (");
199
+ // Keep the whole `for (...)` on one line with normal statement indentation (do not
200
+ // mix bare `write("for (")` with `writeln(")")`, which indents `)` on a new line).
201
+ let mut header = self.indent_str();
202
+ header.push_str("for (");
183
203
  if let Some(i) = init {
184
204
  match i.as_ref() {
185
205
  Statement::VarDecl {
@@ -192,32 +212,32 @@ impl Codegen {
192
212
  let escaped = Self::escape_ident(name.as_ref());
193
213
  if let Some(e) = opt_init {
194
214
  let ex = self.emit_expr(e)?;
195
- self.write(&format!("{} {} = {}", decl, escaped, ex));
215
+ header.push_str(&format!("{} {} = {}", decl, escaped, ex));
196
216
  } else {
197
- self.write(&format!("{} {}", decl, escaped));
217
+ header.push_str(&format!("{} {}", decl, escaped));
198
218
  }
199
219
  }
200
220
  Statement::ExprStmt { expr, .. } => {
201
221
  let ex = self.emit_expr(expr)?;
202
- self.write(&ex);
222
+ header.push_str(&ex);
203
223
  }
204
224
  _ => return Err(CompileError::new("Unsupported for init")),
205
225
  }
206
226
  }
207
- self.write("; ");
227
+ header.push_str("; ");
208
228
  if let Some(c) = cond {
209
229
  let ce = self.emit_expr(c)?;
210
- self.write(&ce);
230
+ header.push_str(&ce);
211
231
  }
212
- self.write("; ");
232
+ header.push_str("; ");
213
233
  if let Some(u) = update {
214
234
  let ue = self.emit_expr(u)?;
215
- self.write(&ue);
235
+ header.push_str(&ue);
216
236
  }
217
- self.writeln(")");
218
- self.indent += 1;
219
- self.emit_statement(body)?;
220
- self.indent -= 1;
237
+ header.push(')');
238
+ header.push('\n');
239
+ self.output.push_str(&header);
240
+ self.emit_js_control_body(body)?;
221
241
  }
222
242
  Statement::ForOf {
223
243
  name,
@@ -228,9 +248,7 @@ impl Codegen {
228
248
  let escaped = Self::escape_ident(name.as_ref());
229
249
  let it = self.emit_expr(iterable)?;
230
250
  self.writeln(&format!("for (const {} of {})", escaped, it));
231
- self.indent += 1;
232
- self.emit_statement(body)?;
233
- self.indent -= 1;
251
+ self.emit_js_control_body(body)?;
234
252
  }
235
253
  Statement::Return { value, .. } => {
236
254
  if let Some(v) = value {
@@ -303,4 +303,48 @@ fn factory() {
303
303
  &js[..800.min(js.len())]
304
304
  );
305
305
  }
306
+
307
+ #[test]
308
+ fn fn_body_two_lets_not_split_by_closing_brace() {
309
+ let src = "fn h() {\n let a = 1\n let b = 2\n}\n";
310
+ let program = parse(src).expect("parse");
311
+ let js = compile_with_jsx(&program, false).expect("compile");
312
+ let i = js.find("let a = 1").expect("let a");
313
+ let j = js.find("let b = 2").expect("let b");
314
+ assert!(
315
+ !js[i..j].contains('}'),
316
+ "first let must not end in an inner block before second let (regression #43): {:?}",
317
+ &js[i..j]
318
+ );
319
+ }
320
+
321
+ #[test]
322
+ fn control_flow_wraps_lexical_decl_body_in_block_for_valid_js() {
323
+ let src = r#"fn f() {
324
+ if (true)
325
+ const x = 1
326
+ while (false)
327
+ let y = 2
328
+ for (;;)
329
+ const z = 3
330
+ for (const v of [])
331
+ let w = 4
332
+ }"#;
333
+ let program = parse(src).expect("parse");
334
+ let js = compile_with_jsx(&program, false).expect("compile");
335
+ for (label, key, decl) in [
336
+ ("if", "if (true)", "const x = 1"),
337
+ ("while", "while (false)", "let y = 2"),
338
+ ("for", "for (; ; )", "const z = 3"),
339
+ ("for-of", "for (const v of [])", "let w = 4"),
340
+ ] {
341
+ let i = js.find(key).expect(label);
342
+ let j = js.find(decl).expect(label);
343
+ assert!(
344
+ i < j && js[i..j].contains('{'),
345
+ "{label}: expected '{{' between {key:?} and {decl:?}, got {:?}",
346
+ &js[i..j]
347
+ );
348
+ }
349
+ }
306
350
  }
@@ -252,4 +252,78 @@ 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!(stmts.len(), 3, "expected expr; const; if as siblings — got {stmts:?}");
324
+ assert!(matches!(stmts[0], Statement::ExprStmt { .. }));
325
+ assert!(matches!(stmts[1], Statement::VarDecl { .. }));
326
+ assert!(matches!(stmts[2], Statement::If { .. }));
327
+ }
328
+
255
329
  }
@@ -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
  }
@@ -938,6 +959,11 @@ impl<'a> Parser<'a> {
938
959
  self.expect(TokenKind::Semicolon)?;
939
960
  Some(c)
940
961
  };
962
+ // `for (init; ; update)` — when the condition is empty we matched `;` above but did not
963
+ // consume it; skip it so `update` / `)` parse correctly (e.g. `for (;;)`).
964
+ if cond.is_none() && matches!(self.peek_kind(), Some(TokenKind::Semicolon)) {
965
+ self.advance();
966
+ }
941
967
  let update = if matches!(self.peek_kind(), Some(TokenKind::RParen)) {
942
968
  None
943
969
  } else {
@@ -1483,7 +1509,7 @@ impl<'a> Parser<'a> {
1483
1509
  TokenKind::Dot | TokenKind::OptionalChain => {
1484
1510
  let optional = kind == TokenKind::OptionalChain;
1485
1511
  self.advance();
1486
- let prop_tok = self.expect(TokenKind::Ident)?;
1512
+ let prop_tok = self.expect_ident_or_type_member_name()?;
1487
1513
  let prop = prop_tok
1488
1514
  .literal
1489
1515
  .clone()
@@ -1603,7 +1629,7 @@ impl<'a> Parser<'a> {
1603
1629
  TokenKind::Dot | TokenKind::OptionalChain => {
1604
1630
  let optional = kind == TokenKind::OptionalChain;
1605
1631
  self.advance();
1606
- let prop_tok = self.expect(TokenKind::Ident)?;
1632
+ let prop_tok = self.expect_ident_or_type_member_name()?;
1607
1633
  let prop = prop_tok
1608
1634
  .literal
1609
1635
  .clone()
@@ -1997,7 +2023,8 @@ impl<'a> Parser<'a> {
1997
2023
  self.expect(TokenKind::RBrace)?; // }
1998
2024
  props.push(JsxProp::Spread(expr));
1999
2025
  }
2000
- Some(TokenKind::Ident) => {
2026
+ // `type` is `TokenKind::Type` but valid as a JSX attr name; see docs/js-emit-philosophy.md.
2027
+ Some(TokenKind::Ident) | Some(TokenKind::Type) => {
2001
2028
  let name_tok = self.advance().unwrap();
2002
2029
  let name = name_tok.literal.clone().ok_or("Expected attr name")?;
2003
2030
  if matches!(self.peek_kind(), Some(TokenKind::Assign)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file