@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.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.0.22"
3
+ version = "1.0.26"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -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
- let bytes = (new_offset as i16).to_be_bytes();
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) => self.emit_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 let Some(depth) = self.template_brace_stack.last_mut() { *depth += 1; }
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 { TokenKind::Gt }
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 { TokenKind::Slash }
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
- /// Push text child, merging with previous Text if any (preserves spaces between tokens).
1565
- fn push_or_merge_text(&self, children: &mut Vec<JsxChild>, s: Arc<str>) {
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 merged = format!("{} {}", prev.as_ref(), s.as_ref());
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
- return Err(format!("Unexpected token in JSX children: {:?}", self.peek_kind()));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.0.22",
3
+ "version": "1.0.26",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, compile to native.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file