@tishlang/tish 1.0.22 → 1.0.26
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/crates/tish/Cargo.toml +1 -1
- package/crates/tish_bytecode/src/peephole.rs +1 -5
- package/crates/tish_compile_js/src/codegen.rs +18 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +17 -0
- package/crates/tish_lexer/src/lib.rs +72 -4
- package/crates/tish_lexer/src/token.rs +2 -0
- package/crates/tish_parser/src/parser.rs +94 -11
- 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/crates/tish/Cargo.toml
CHANGED
|
@@ -133,11 +133,7 @@ fn chain_jumps(code: &mut [u8]) {
|
|
|
133
133
|
if let Some(final_target) = final_jump_target(code, ip) {
|
|
134
134
|
if final_target != current_target {
|
|
135
135
|
let new_offset = final_target as i32 - (ip + 3) as i32;
|
|
136
|
-
|
|
137
|
-
if ip + 2 < code.len() {
|
|
138
|
-
code[ip + 1] = bytes[0];
|
|
139
|
-
code[ip + 2] = bytes[1];
|
|
140
|
-
}
|
|
136
|
+
write_u16(code, ip + 1, (new_offset as i16) as u16);
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
139
|
}
|
|
@@ -733,7 +733,24 @@ impl Codegen {
|
|
|
733
733
|
fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
|
|
734
734
|
match child {
|
|
735
735
|
JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
|
|
736
|
-
JsxChild::Expr(e) =>
|
|
736
|
+
JsxChild::Expr(e) => {
|
|
737
|
+
let inner = self.emit_expr(e)?;
|
|
738
|
+
let needs_string = !matches!(
|
|
739
|
+
e,
|
|
740
|
+
Expr::Literal {
|
|
741
|
+
value: Literal::String(_),
|
|
742
|
+
..
|
|
743
|
+
}
|
|
744
|
+
| Expr::TemplateLiteral { .. }
|
|
745
|
+
| Expr::JsxElement { .. }
|
|
746
|
+
| Expr::JsxFragment { .. }
|
|
747
|
+
);
|
|
748
|
+
Ok(if needs_string {
|
|
749
|
+
format!("String({})", inner)
|
|
750
|
+
} else {
|
|
751
|
+
inner
|
|
752
|
+
})
|
|
753
|
+
}
|
|
737
754
|
}
|
|
738
755
|
}
|
|
739
756
|
}
|
|
@@ -51,6 +51,23 @@ mod tests {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
#[test]
|
|
55
|
+
fn jsx_text_punctuation_no_space() {
|
|
56
|
+
// Punctuation (e.g. !) concatenates without space: "work!" not "work !"
|
|
57
|
+
let src = r#"fn X() { return <p>work!</p> }"#;
|
|
58
|
+
let program = parse(src).unwrap();
|
|
59
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
60
|
+
assert!(js.contains(r#""work!""#), "expected 'work!', got: {}", &js[..400.min(js.len())]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[test]
|
|
64
|
+
fn jsx_text_emojis() {
|
|
65
|
+
let src = r#"fn X() { return <p>hello 😔</p> }"#;
|
|
66
|
+
let program = parse(src).unwrap();
|
|
67
|
+
let js = compile_with_jsx(&program, false, JsxMode::LattishH).unwrap();
|
|
68
|
+
assert!(js.contains("😔"), "expected emoji, got: {}", &js[..400.min(js.len())]);
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
#[test]
|
|
55
72
|
fn jsx_text_whitespace_via_compile_project() {
|
|
56
73
|
let dir = std::env::temp_dir().join("tishlang_compile_project_test");
|
|
@@ -24,6 +24,13 @@ pub struct Lexer<'a> {
|
|
|
24
24
|
at_line_start: bool,
|
|
25
25
|
pending_dedents: VecDeque<Token>,
|
|
26
26
|
template_brace_stack: Vec<usize>,
|
|
27
|
+
jsx_after_gt: bool,
|
|
28
|
+
jsx_in_opening_tag: bool,
|
|
29
|
+
jsx_saw_slash_before_gt: bool,
|
|
30
|
+
jsx_brace_depth: i32,
|
|
31
|
+
jsx_depth: i32,
|
|
32
|
+
jsx_child_brace_depth: i32,
|
|
33
|
+
jsx_in_closing_tag: bool,
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
impl<'a> Lexer<'a> {
|
|
@@ -37,6 +44,29 @@ impl<'a> Lexer<'a> {
|
|
|
37
44
|
at_line_start: true,
|
|
38
45
|
pending_dedents: VecDeque::new(),
|
|
39
46
|
template_brace_stack: Vec::new(),
|
|
47
|
+
jsx_after_gt: false,
|
|
48
|
+
jsx_in_opening_tag: false,
|
|
49
|
+
jsx_saw_slash_before_gt: false,
|
|
50
|
+
jsx_brace_depth: 0,
|
|
51
|
+
jsx_depth: 0,
|
|
52
|
+
jsx_child_brace_depth: 0,
|
|
53
|
+
jsx_in_closing_tag: false,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn read_jsx_text(&mut self, start: (usize, usize)) -> Result<Option<Token>, String> {
|
|
58
|
+
let mut s = String::new();
|
|
59
|
+
loop {
|
|
60
|
+
match self.peek() {
|
|
61
|
+
None | Some('{') | Some('<') => break,
|
|
62
|
+
Some(c) => { self.advance(); s.push(c); }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if s.is_empty() {
|
|
66
|
+
Ok(None)
|
|
67
|
+
} else {
|
|
68
|
+
let end = self.span_start();
|
|
69
|
+
Ok(Some(Token { kind: TokenKind::JsxText, span: Span { start, end }, literal: Some(s.into()) }))
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
|
|
@@ -227,6 +257,16 @@ impl<'a> Lexer<'a> {
|
|
|
227
257
|
return Ok(Some(tok));
|
|
228
258
|
}
|
|
229
259
|
|
|
260
|
+
if self.jsx_after_gt {
|
|
261
|
+
self.jsx_after_gt = false;
|
|
262
|
+
if !matches!(self.peek(), Some('{') | Some('<') | None) {
|
|
263
|
+
let start = self.span_start();
|
|
264
|
+
if let Some(tok) = self.read_jsx_text(start)? {
|
|
265
|
+
return Ok(Some(tok));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
230
270
|
if self.at_line_start {
|
|
231
271
|
self.at_line_start = false;
|
|
232
272
|
let level = self.read_indent_level();
|
|
@@ -265,10 +305,19 @@ impl<'a> Lexer<'a> {
|
|
|
265
305
|
'(' => TokenKind::LParen,
|
|
266
306
|
')' => TokenKind::RParen,
|
|
267
307
|
'{' => {
|
|
268
|
-
if
|
|
308
|
+
if self.jsx_in_opening_tag { self.jsx_brace_depth += 1; }
|
|
309
|
+
else if self.jsx_depth > 0 { self.jsx_child_brace_depth += 1; }
|
|
310
|
+
if let Some(depth) = self.template_brace_stack.last_mut() {
|
|
311
|
+
*depth += 1;
|
|
312
|
+
}
|
|
269
313
|
TokenKind::LBrace
|
|
270
314
|
}
|
|
271
315
|
'}' => {
|
|
316
|
+
if self.jsx_brace_depth > 0 { self.jsx_brace_depth -= 1; }
|
|
317
|
+
else if self.jsx_child_brace_depth > 0 {
|
|
318
|
+
self.jsx_child_brace_depth -= 1;
|
|
319
|
+
if self.jsx_child_brace_depth == 0 { self.jsx_after_gt = true; }
|
|
320
|
+
}
|
|
272
321
|
if let Some(depth) = self.template_brace_stack.last_mut() {
|
|
273
322
|
*depth -= 1;
|
|
274
323
|
if *depth == 0 {
|
|
@@ -306,12 +355,28 @@ impl<'a> Lexer<'a> {
|
|
|
306
355
|
'<' => {
|
|
307
356
|
if self.peek() == Some('=') { self.advance(); TokenKind::Le }
|
|
308
357
|
else if self.peek() == Some('<') { self.advance(); TokenKind::Shl }
|
|
309
|
-
else { TokenKind::Lt }
|
|
358
|
+
else if self.peek() == Some('/') { self.jsx_in_closing_tag = true; TokenKind::Lt }
|
|
359
|
+
else if self.peek() == Some('>') || self.peek().map(|c| c.is_ascii_alphabetic() || c == '_').unwrap_or(false) {
|
|
360
|
+
self.jsx_depth += 1;
|
|
361
|
+
self.jsx_in_opening_tag = true;
|
|
362
|
+
TokenKind::Lt
|
|
363
|
+
} else { TokenKind::Lt }
|
|
310
364
|
}
|
|
311
365
|
'>' => {
|
|
312
366
|
if self.peek() == Some('=') { self.advance(); TokenKind::Ge }
|
|
313
367
|
else if self.peek() == Some('>') { self.advance(); TokenKind::Shr }
|
|
314
|
-
else {
|
|
368
|
+
else {
|
|
369
|
+
if self.jsx_in_opening_tag && self.jsx_brace_depth == 0 && !self.jsx_saw_slash_before_gt {
|
|
370
|
+
self.jsx_after_gt = true;
|
|
371
|
+
}
|
|
372
|
+
if self.jsx_in_closing_tag || (self.jsx_in_opening_tag && self.jsx_saw_slash_before_gt) {
|
|
373
|
+
self.jsx_depth = (self.jsx_depth - 1).max(0);
|
|
374
|
+
}
|
|
375
|
+
self.jsx_in_opening_tag = false;
|
|
376
|
+
self.jsx_in_closing_tag = false;
|
|
377
|
+
self.jsx_saw_slash_before_gt = false;
|
|
378
|
+
TokenKind::Gt
|
|
379
|
+
}
|
|
315
380
|
}
|
|
316
381
|
'^' => TokenKind::BitXor,
|
|
317
382
|
'~' => TokenKind::BitNot,
|
|
@@ -334,7 +399,10 @@ impl<'a> Lexer<'a> {
|
|
|
334
399
|
if self.peek() == Some('/') { self.advance(); self.skip_line_comment(); return self.next_token(); }
|
|
335
400
|
else if self.peek() == Some('*') { self.advance(); self.skip_block_comment()?; return self.next_token(); }
|
|
336
401
|
else if self.peek() == Some('=') { self.advance(); TokenKind::SlashAssign }
|
|
337
|
-
else {
|
|
402
|
+
else {
|
|
403
|
+
if self.jsx_in_opening_tag { self.jsx_saw_slash_before_gt = true; }
|
|
404
|
+
TokenKind::Slash
|
|
405
|
+
}
|
|
338
406
|
}
|
|
339
407
|
'%' => {
|
|
340
408
|
if self.peek() == Some('=') { self.advance(); TokenKind::PercentAssign }
|
|
@@ -115,6 +115,8 @@ pub enum TokenKind {
|
|
|
115
115
|
TemplateHead, // `text${ (start with interpolation)
|
|
116
116
|
TemplateMiddle, // }text${ (middle part)
|
|
117
117
|
TemplateTail, // }text` (end part)
|
|
118
|
+
|
|
119
|
+
JsxText, // Raw text in JSX children (emojis, etc.); only {}<> are special
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
impl TokenKind {
|
|
@@ -1561,10 +1561,54 @@ impl<'a> Parser<'a> {
|
|
|
1561
1561
|
})
|
|
1562
1562
|
}
|
|
1563
1563
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1564
|
+
fn token_as_jsx_text(kind: TokenKind) -> Option<&'static str> {
|
|
1565
|
+
use TokenKind::*;
|
|
1566
|
+
match kind {
|
|
1567
|
+
Not => Some("!"),
|
|
1568
|
+
Question => Some("?"),
|
|
1569
|
+
Dot => Some("."),
|
|
1570
|
+
Comma => Some(","),
|
|
1571
|
+
Colon => Some(":"),
|
|
1572
|
+
Semicolon => Some(";"),
|
|
1573
|
+
Plus => Some("+"),
|
|
1574
|
+
Minus => Some("-"),
|
|
1575
|
+
Star => Some("*"),
|
|
1576
|
+
Slash => Some("/"),
|
|
1577
|
+
Percent => Some("%"),
|
|
1578
|
+
Eq | Assign => Some("="),
|
|
1579
|
+
Gt => Some(">"),
|
|
1580
|
+
Le => Some("<="),
|
|
1581
|
+
Ge => Some(">="),
|
|
1582
|
+
Ne => Some("!="),
|
|
1583
|
+
StrictEq => Some("==="),
|
|
1584
|
+
StrictNe => Some("!=="),
|
|
1585
|
+
BitAnd => Some("&"),
|
|
1586
|
+
BitOr => Some("|"),
|
|
1587
|
+
BitXor => Some("^"),
|
|
1588
|
+
BitNot => Some("~"),
|
|
1589
|
+
And => Some("&&"),
|
|
1590
|
+
Or => Some("||"),
|
|
1591
|
+
LParen => Some("("),
|
|
1592
|
+
RParen => Some(")"),
|
|
1593
|
+
LBracket => Some("["),
|
|
1594
|
+
RBracket => Some("]"),
|
|
1595
|
+
PlusPlus => Some("++"),
|
|
1596
|
+
MinusMinus => Some("--"),
|
|
1597
|
+
StarStar => Some("**"),
|
|
1598
|
+
Arrow => Some("=>"),
|
|
1599
|
+
OptionalChain => Some("?."),
|
|
1600
|
+
NullishCoalesce => Some("??"),
|
|
1601
|
+
Shl => Some("<<"),
|
|
1602
|
+
Shr => Some(">>"),
|
|
1603
|
+
_ => None,
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/// Merge text. Add space between words (Ident/Number/String); no space before/after punctuation.
|
|
1608
|
+
fn push_or_merge_text(&self, children: &mut Vec<JsxChild>, s: Arc<str>, is_punctuation: bool) {
|
|
1566
1609
|
if let Some(JsxChild::Text(prev)) = children.last() {
|
|
1567
|
-
let
|
|
1610
|
+
let sep = if is_punctuation { "" } else { " " };
|
|
1611
|
+
let merged = format!("{}{}{}", prev.as_ref(), sep, s.as_ref());
|
|
1568
1612
|
let last = children.len() - 1;
|
|
1569
1613
|
children[last] = JsxChild::Text(Arc::from(merged.as_str()));
|
|
1570
1614
|
} else {
|
|
@@ -1608,23 +1652,41 @@ impl<'a> Parser<'a> {
|
|
|
1608
1652
|
self.expect(TokenKind::RBrace)?; // }
|
|
1609
1653
|
children.push(JsxChild::Expr(expr));
|
|
1610
1654
|
}
|
|
1655
|
+
Some(TokenKind::JsxText) => {
|
|
1656
|
+
let t = self.advance().unwrap();
|
|
1657
|
+
let s = t.literal.clone().unwrap_or_default();
|
|
1658
|
+
if !s.is_empty() {
|
|
1659
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1611
1662
|
Some(TokenKind::String) => {
|
|
1612
1663
|
let t = self.advance().unwrap();
|
|
1613
1664
|
let s = t.literal.clone().unwrap_or_default();
|
|
1614
1665
|
if !s.is_empty() {
|
|
1615
|
-
self.push_or_merge_text(&mut children, s);
|
|
1666
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
Some(TokenKind::Number) => {
|
|
1670
|
+
let t = self.advance().unwrap();
|
|
1671
|
+
let s = t.literal.clone().unwrap_or_default();
|
|
1672
|
+
if !s.is_empty() {
|
|
1673
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1616
1674
|
}
|
|
1617
1675
|
}
|
|
1618
1676
|
Some(TokenKind::Ident) => {
|
|
1619
|
-
// Bare identifiers in JSX are text (e.g. "hello" in <div>hello</div>)
|
|
1620
1677
|
let t = self.advance().unwrap();
|
|
1621
1678
|
let s = t.literal.clone().unwrap_or_default();
|
|
1622
1679
|
if !s.is_empty() {
|
|
1623
|
-
self.push_or_merge_text(&mut children, s);
|
|
1680
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1624
1681
|
}
|
|
1625
1682
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1683
|
+
Some(k) => {
|
|
1684
|
+
if let Some(s) = Self::token_as_jsx_text(k) {
|
|
1685
|
+
self.advance();
|
|
1686
|
+
self.push_or_merge_text(&mut children, Arc::from(s), true);
|
|
1687
|
+
} else {
|
|
1688
|
+
return Err(format!("Unexpected token in JSX children: {:?}", k));
|
|
1689
|
+
}
|
|
1628
1690
|
}
|
|
1629
1691
|
}
|
|
1630
1692
|
}
|
|
@@ -1666,21 +1728,42 @@ impl<'a> Parser<'a> {
|
|
|
1666
1728
|
self.expect(TokenKind::RBrace)?;
|
|
1667
1729
|
children.push(JsxChild::Expr(expr));
|
|
1668
1730
|
}
|
|
1731
|
+
Some(TokenKind::JsxText) => {
|
|
1732
|
+
let t = self.advance().unwrap();
|
|
1733
|
+
let s = t.literal.clone().unwrap_or_default();
|
|
1734
|
+
if !s.is_empty() {
|
|
1735
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1669
1738
|
Some(TokenKind::String) => {
|
|
1670
1739
|
let t = self.advance().unwrap();
|
|
1671
1740
|
let s = t.literal.clone().unwrap_or_default();
|
|
1672
1741
|
if !s.is_empty() {
|
|
1673
|
-
self.push_or_merge_text(&mut children, s);
|
|
1742
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
Some(TokenKind::Number) => {
|
|
1746
|
+
let t = self.advance().unwrap();
|
|
1747
|
+
let s = t.literal.clone().unwrap_or_default();
|
|
1748
|
+
if !s.is_empty() {
|
|
1749
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1674
1750
|
}
|
|
1675
1751
|
}
|
|
1676
1752
|
Some(TokenKind::Ident) => {
|
|
1677
1753
|
let t = self.advance().unwrap();
|
|
1678
1754
|
let s = t.literal.clone().unwrap_or_default();
|
|
1679
1755
|
if !s.is_empty() {
|
|
1680
|
-
self.push_or_merge_text(&mut children, s);
|
|
1756
|
+
self.push_or_merge_text(&mut children, s, false);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
Some(k) => {
|
|
1760
|
+
if let Some(s) = Self::token_as_jsx_text(k) {
|
|
1761
|
+
self.advance();
|
|
1762
|
+
self.push_or_merge_text(&mut children, Arc::from(s), true);
|
|
1763
|
+
} else {
|
|
1764
|
+
return Err(format!("Unexpected token in JSX fragment: {:?}", k));
|
|
1681
1765
|
}
|
|
1682
1766
|
}
|
|
1683
|
-
_ => return Err(format!("Unexpected token in JSX fragment: {:?}", self.peek_kind())),
|
|
1684
1767
|
}
|
|
1685
1768
|
}
|
|
1686
1769
|
}
|
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
|