@tishlang/tish 1.9.1 → 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 +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish_compile_js/src/codegen.rs +42 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_parser/src/lib.rs +74 -0
- package/crates/tish_parser/src/parser.rs +31 -4
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
package/bin/tish
CHANGED
|
Binary file
|
package/crates/tish/Cargo.toml
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
header.push_str(&format!("{} {} = {}", decl, escaped, ex));
|
|
196
216
|
} else {
|
|
197
|
-
|
|
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
|
-
|
|
222
|
+
header.push_str(&ex);
|
|
203
223
|
}
|
|
204
224
|
_ => return Err(CompileError::new("Unsupported for init")),
|
|
205
225
|
}
|
|
206
226
|
}
|
|
207
|
-
|
|
227
|
+
header.push_str("; ");
|
|
208
228
|
if let Some(c) = cond {
|
|
209
229
|
let ce = self.emit_expr(c)?;
|
|
210
|
-
|
|
230
|
+
header.push_str(&ce);
|
|
211
231
|
}
|
|
212
|
-
|
|
232
|
+
header.push_str("; ");
|
|
213
233
|
if let Some(u) = update {
|
|
214
234
|
let ue = self.emit_expr(u)?;
|
|
215
|
-
|
|
235
|
+
header.push_str(&ue);
|
|
216
236
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
self.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|